SDK Reference
Import:
import "github.com/TDB-Group/anvil/sdk"This page lists the public SDK surface used by application code.
The SDK contains three kinds of types:
- Application-facing types, such as markers,
sdk.Ctx, middleware contracts, WebSocket messages, GraphQL requests, gRPC stream contracts, and queue messages. - Generated-code contracts, such as route, service, and job descriptors.
- Driver-author contracts, such as
HTTPTransport,GraphQLTransport,CodecRegistry, andErrorPipeline.
Most backends use the first group directly and see the second group only inside generated code.
Markers
Section titled “Markers”type Controller struct{}type Group struct{}type Bundle struct{}type HttpEndpoint struct{}type GrpcEndpoint struct{}type GraphQLEndpoint struct{}type GraphQLEndpointWith[Policy any] struct{}type QueueJob struct{}These are empty type markers. The compiler uses Go type identity to detect them.
Bundle groups dependency fields together. Only fields tagged with inject
are wired:
type Services struct { sdk.Bundle
ReadStore project.Store `inject:"read"` WriteStore project.Store `inject:"write"`}HTTP Route Types
Section titled “HTTP Route Types”type GET struct{}type POST struct{}type PUT struct{}type PATCH struct{}type DELETE struct{}type WS struct{}
type GETWith[Policy any] struct{}type POSTWith[Policy any] struct{}type PUTWith[Policy any] struct{}type PATCHWith[Policy any] struct{}type DELETEWith[Policy any] struct{}type WSWith[Policy any] struct{}
type Use[Middleware any] struct{}Use method markers when no policy is needed. Use *With[T] when the route
needs a policy bundle.
The compiler reads path tags on route marker fields:
type Projects struct { sdk.Controller `path:"/projects"`
Routes struct { List sdk.GET `path:"/"` Get sdk.GET `path:"/:projectId"` Save sdk.POST `path:"/"` }}Anvil route params use :name syntax. OpenAPI output converts those params to
{name} because that is the OpenAPI path format.
Generated HTTP metadata uses these descriptors:
type HTTPHandler func(ctx Ctx) (any, error)
type HTTPRoute struct { Method string Path string Controller string Endpoint string Handler HTTPHandler Middleware []HTTPMiddlewareComponent}
type WebSocketHandler func(ctx Ctx, socket WebSocket) error
type WebSocketRoute struct { Path string Controller string Endpoint string Handler WebSocketHandler Middleware []HTTPMiddlewareComponent Subprotocols []string}Application code normally declares route markers and handler methods. Generated
code builds HTTPRoute and WebSocketRoute values for the selected driver.
Request Context
Section titled “Request Context”type Ctx interface { Context() context.Context Native() any Request() HTTPRequest Response() HTTPResponse Locals() LocalStore Errors() ErrorFactory Next() (any, error)}Native() is intentionally typed as any. Type assert to the selected
driver’s native context when a handler deliberately opts into that driver.
Next() exists because middleware and handlers share the same context type.
Handlers return their normal (body, error) result. Official drivers install
Next only while a middleware value is running; calling it outside that window
returns an internal failure.
HTTP Request
Section titled “HTTP Request”type HTTPRequest interface { Method() string Path() string IP() string Param(name string) string Query(name string) string Header(name string) string Cookie(name string) string Body() []byte Decode(out any) error}IP() returns the verified client IP. With anvil.WithProxy, it uses the
trusted proxy rules configured at the edge. Without proxy config, it returns the
direct socket peer IP when the driver can expose it.
Cookie(name) returns the named request cookie value or an empty string when
the cookie is missing.
Decode uses the driver-installed codec registry.
Body() has no error return. In the standard-library driver, a body read error
returns nil; use Decode when the caller needs body read and codec failures
to flow through the error pipeline. The Fiber driver reads from Fiber’s buffered
request body.
HTTP Response
Section titled “HTTP Response”type HTTPResponse interface { Status(code int) Header(name string, value string) Cookie(cookie *http.Cookie) Stream(handler func(HTTPStream) error) error}Handlers return the response body as the handler result. Response() is for
status, headers, cookies, and streaming.
Cookie(cookie) appends a Set-Cookie header. Use it instead of
Header("Set-Cookie", ...) so multiple cookies are preserved.
Stream marks the response as streaming. A handler that configures a stream
must return nil as the body; drivers reject “stream plus body” because there
is no portable response shape for both.
The official HTTP drivers validate response metadata before writing. Empty
header names and status codes outside 100..999 become handler errors and go
through the normal error pipeline. Setting the same header name again replaces
the earlier value.
Streaming
Section titled “Streaming”type HTTPStream interface { Context() context.Context Write(data []byte) error Flush() error}Locals
Section titled “Locals”type LocalStore interface { Get(name string) any Set(name string, value any)}Use locals to pass request-scoped middleware state to later middleware and handlers.
Official HTTP drivers ignore empty local names on Set and return nil for an
empty name on Get. Treat locals as request state, not as a dependency lookup
mechanism.
Error Factory
Section titled “Error Factory”type ErrorFactory interface { Failure(status int, message string) error NotFound(resource string) error InvalidParam(name string, cause error) error Validation() ValidationBuilder Wrap(cause error, operation string) error}
type ValidationBuilder interface { Field(name string, message string) ValidationBuilder Err() error}Wrap is for unexpected technical failures. Failure, NotFound,
InvalidParam, and Validation are for expected client/domain failures.
The core error factory implements these defaults:
Failure(status, message)normalizes invalid status codes to500. Status500is internal and setsExpected: false; other valid status codes setExpected: true.NotFound(resource)returns404with message"<resource> not found". Empty resource names become"resource".InvalidParam(name, cause)returns400with public messageinvalid request, field message"invalid value", and the technical cause attached. Empty names become"param".Validation().Field(name, message).Err()returns400with all collected field messages. Empty field names are ignored. Empty messages become"invalid value".Wrap(cause, operation)returns500with public messageinternal server error, wraps the cause as"<operation>: <cause>", and captures stack frames. Nil causes become"missing cause". Empty operations become"operation".
Errors
Section titled “Errors”Expected failures use standard HTTP status codes from net/http:
return nil, ctx.Errors().Failure(http.StatusForbidden, "admin access required")There is no Anvil-owned error-code enum. Use ctx.Errors().Wrap for
unexpected technical failures.
type ErrorContext struct { Protocol string Controller string Endpoint string Method string Route string Path string RequestID string TraceID string Phase ErrorPhase Attrs map[string]any}
type Failure struct { Status int Message string Fields map[string]string Attrs map[string]any Cause error Context ErrorContext Stack []Frame Expected bool}
func (f *Failure) Error() stringfunc (f *Failure) Unwrap() error
type Frame struct { Function string File string Line int}
type ErrorMapper interface { MapError(ctx ErrorContext, err error) (Failure, bool)}
type ErrorPipeline interface { Use(mapper ErrorMapper) Replace(mapper ErrorMapper) Map(ctx context.Context, failureCtx ErrorContext, err error) Failure Publish(ctx context.Context, failure Failure)}
type ErrorEvent struct { Error error Failure Failure Expected bool Recovered bool}Failure.Error() returns the public message. It does not include the cause,
because causes can contain database details, upstream payloads, or other
implementation data. If Message is empty, a 500 or unset status returns
internal server error; other known statuses return the lowercase
net/http.StatusText value; unknown statuses return request failed.
Failure.Unwrap() exposes Cause for errors.Is and errors.As.
The default mapper treats *sdk.Failure as already classified. Any other error
becomes a 500 with public message internal server error. Invalid status
codes are normalized to 500.
Use appends custom mappers before the fallback mapper. They run in
registration order and the first mapper that returns true wins. Replace
swaps the fallback mapper; it does not remove mappers already added through
Use.
Pipeline normalization always applies after custom mappers run. It normalizes
status codes, fills an empty public message, attaches the mapped context, creates
empty Fields, Attrs, and Context.Attrs maps when they are nil, and captures
stack frames for internal failures without an existing stack.
Failure context is merged field by field. The driver or generated-code context
is the base. A failure returned by application code can override specific
context fields by setting them on Failure.Context; empty fields leave the base
context intact. Context.Attrs maps are merged, with application-provided
attributes winning on duplicate keys.
Observers run synchronously in registration order. Observers that write to external systems need their own timeout and panic recovery.
ErrorEvent.Error is the normalized failure cause. Recovered is true when
the final failure phase is panic.
ErrorContext.Phase uses these exported values:
const ( ErrorPhaseBind ErrorPhase = "bind" ErrorPhaseDecode ErrorPhase = "decode" ErrorPhasePolicy ErrorPhase = "policy" ErrorPhaseHandler ErrorPhase = "handler" ErrorPhaseEncode ErrorPhase = "encode" ErrorPhaseTransport ErrorPhase = "transport" ErrorPhasePanic ErrorPhase = "panic")Drivers and generated code use phases to describe where a failure surfaced without leaking framework-specific internals into plugins.
Middleware
Section titled “Middleware”Middleware is attached with sdk.Use[T]. See
Middleware for the generated execution
model.
The method name decides the protocol. BeforeHTTP, HandleHTTP,
AfterHTTP, and OnHTTPError are HTTP contracts. HandleGraphQL,
HandleGRPC, and HandleQueue are the GraphQL, gRPC, and queue contracts.
These methods run per request, operation, RPC, or job delivery. Plugin
lifecycle hooks such as OnBoot and OnShutdown are separate startup and
shutdown callbacks.
ctx.Next() belongs to middleware. It advances to the next generated
middleware value or final handler and can be called once per middleware
invocation. HTTP handlers receive sdk.Ctx for request state, response
metadata, locals, and error helpers; they finish by returning (body, error).
type HTTPMiddleware interface { HandleHTTP(ctx Ctx) (any, error)}
type HTTPMiddlewareComponent interface{}
type HTTPBeforeMiddleware interface { BeforeHTTP(ctx Ctx) error}
type HTTPAfterMiddleware interface { AfterHTTP(ctx Ctx, body any, err error) (any, error)}
type HTTPErrorMiddleware interface { OnHTTPError(ctx Ctx, err error) error}
type GraphQLMiddleware interface { HandleGraphQL(ctx GraphQLCtx) (GraphQLResponse, error)}
type GraphQLMiddlewareComponent interface{}
type GRPCMiddleware interface { HandleGRPC(ctx GRPCCtx) (any, error)}
type GRPCMiddlewareComponent interface{}
type QueueMiddleware interface { HandleQueue(ctx QueueCtx) error}
type QueueMiddlewareComponent interface{}The *MiddlewareComponent interfaces are intentionally empty. Generated route
metadata needs one slice type per protocol, while the compiler and drivers
validate the concrete method contracts. Application middleware implements the
protocol method it needs; the component interfaces are metadata container
types.
One middleware type can implement more than one protocol method. The generated chain still stays protocol-specific: HTTP routes receive HTTP middleware, gRPC methods receive gRPC middleware, GraphQL endpoints receive GraphQL middleware, and queue jobs receive queue middleware.
GraphQL
Section titled “GraphQL”type GraphQLHandler func(context.Context, GraphQLRequest) (GraphQLResponse, error)
type GraphQLSubscriptionHandler func( context.Context, GraphQLRequest, GraphQLSubscriptionStream,) error
type GraphQLCtx interface { Context() context.Context Request() GraphQLRequest Subscription() GraphQLSubscriptionStream Next() (GraphQLResponse, error)}
type GraphQLSubscriptionStream interface { Send(response GraphQLResponse) error}
type GraphQLRequest struct { Query string OperationName string Variables map[string]any Extensions map[string]any}
type GraphQLResponse struct { Data any Errors []GraphQLError Extensions map[string]any}
type GraphQLError struct { Message string Path []any Extensions map[string]any}
type GraphQLRoute struct { Path string Endpoint string Handler GraphQLHandler Subscribe GraphQLSubscriptionHandler Middleware []GraphQLMiddlewareComponent}Application GraphQL endpoints expose Execute and optional Subscribe;
generated code converts those methods into GraphQLRoute.
GraphQLCtx.Subscription() is nil for normal query and mutation execution. It
is populated only when the selected GraphQL driver is running a subscription
path.
type GRPCUnaryHandler func(ctx context.Context, request any) (any, error)
type GRPCStreamHandler func( ctx context.Context, request any, stream GRPCStream,) (any, error)
type GRPCCtx interface { Context() context.Context Service() string Method() string FullMethod() string StreamKind() GRPCStreamKind Request() any Stream() GRPCStream Next() (any, error)}
type GRPCStreamKind string
const ( GRPCStreamUnknown GRPCStreamKind = "" GRPCStreamServer GRPCStreamKind = "server" GRPCStreamClient GRPCStreamKind = "client" GRPCStreamBidi GRPCStreamKind = "bidi")
type GRPCStream interface { Context() context.Context Send(message any) error Recv(message any) error}
type GRPCServerStream[Response any] interface { Context() context.Context Send(Response) error}
type GRPCClientStream[Request any] interface { Context() context.Context Recv() (Request, error)}
type GRPCBidiStream[Request any, Response any] interface { Context() context.Context Recv() (Request, error) Send(Response) error}
type GRPCService struct { Name string Methods []GRPCUnaryMethod Streams []GRPCStreamMethod}
type GRPCUnaryMethod struct { Name string NewRequest func() any Handler GRPCUnaryHandler Middleware []GRPCMiddlewareComponent}
type GRPCStreamMethod struct { Name string Kind GRPCStreamKind NewRequest func() any Handler GRPCStreamHandler Middleware []GRPCMiddlewareComponent}Application handlers use the typed stream interfaces. Generated code and the
driver bridge use the untyped GRPCStream contract.
Unary gRPC middleware sees GRPCStreamUnknown from StreamKind() and receives
the decoded request from Request(). Server-streaming middleware also receives
the decoded request. Client-streaming and bidirectional middleware receive nil
from Request() and use Stream() only when the middleware deliberately needs
raw stream access.
type QueueHandler func(ctx context.Context, message QueueMessage) error
type QueueCtx interface { Context() context.Context Message() QueueMessage Next() error}
type QueueMessage struct { ID string Queue string Body []byte Headers map[string]string Attempt int}
type QueueRoute struct { Queue string Job string Handler QueueHandler Middleware []QueueMiddlewareComponent}Generated code builds QueueRoute values. Queue drivers convert broker
messages into QueueMessage before invoking handlers.
Broker-backed drivers set QueueMessage.Queue from the selected route or
subject. Message IDs, attempts, and headers come from the selected broker and
are documented on the queue driver page.
Codecs
Section titled “Codecs”type EncodedBody struct { ContentType string Body []byte}
type Codec interface { ContentTypes() []string Encode(value any) ([]byte, error) Decode(body []byte, out any) error}
type CodecRegistry interface { Encode(accept string, value any) (EncodedBody, error) Decode(contentType string, body []byte, out any) error}HTTP drivers use the codec registry for ctx.Request().Decode and handler
return values. The SDK defines the contract; driver packages decide which
registry to install. Anvil ships JSON and XML codecs in core/codec, but
application code normally configures codecs through the selected HTTP driver.
The default registry uses the first registered codec as the fallback for empty
Content-Type, empty Accept, and */*. With Anvil’s built-in registry, that
fallback is JSON. Accept values are checked in header order after media type
parameters are removed; quality weights are not sorted.
The built-in registry maps codec errors to Anvil failures:
- Unknown
Acceptreturns406 Not Acceptable. - Unknown
Content-Typereturns415 Unsupported Media Type. - A nil decode target returns
400 Bad Request. - Decode failures return
400 Bad Request. - Encode failures return
500 Internal Server Error. - A nil or invalid registry returns
500 Internal Server Error.
Transport Contracts
Section titled “Transport Contracts”Most applications interact with drivers through constructors. Driver packages implement these contracts:
type Transport interface { Protocol() string Start(addr string) error Shutdown(ctx context.Context) error}
type HTTPTransport interface { Transport RegisterHTTP(route HTTPRoute) error}
type WebSocketTransport interface { HTTPTransport RegisterWebSocket(route WebSocketRoute) error}
type GRPCTransport interface { Transport RegisterGRPC(service GRPCService) error}
type GraphQLTransport interface { Transport RegisterGraphQL(route GraphQLRoute) error}
type QueueTransport interface { Transport RegisterQueueJob(route QueueRoute) error}Route, service, and job descriptors are generated metadata consumed by drivers.
Generated code builds them after app.Wire(...) starts and passes them to the
selected drivers.
Start(addr) is the blocking runtime entrypoint for transports that the core
app starts directly. Queue drivers and custom background transports use that
path. HTTP-family drivers that implement http.Handler are normally mounted
behind Anvil’s shared edge listener, so their standalone Start(addr) method
is not called by Anvil in the normal multi-protocol runtime. The edge
transport receives the public address and forwards selected requests to the
driver handler.
Shutdown(ctx) must be safe to call during graceful app shutdown. Edge-target
drivers still receive shutdown even though their Start(addr) method was not
called by Anvil.
Generated wiring uses fixed protocol strings before it checks the interface:
httpforsdk.HTTPTransportgraphqlforsdk.GraphQLTransportgrpcforsdk.GRPCTransportqueueforsdk.QueueTransport
WebSocket wiring is the exception: it finds the first registered
sdk.WebSocketTransport, normally the HTTP driver. A third-party driver meant
for generated Anvil code uses the standard protocol string for its family.
WebSockets
Section titled “WebSockets”type WebSocket interface { Context() context.Context Native() any Subprotocol() string Read() (WebSocketMessage, error) Write(message WebSocketMessage) error Close(code WebSocketCloseCode, reason string) error}Message constants:
type WebSocketMessage struct { Type WebSocketMessageType Data []byte}
type WebSocketMessageType int
WebSocketMessageUnknownWebSocketTextWebSocketBinaryWebSocketCloseWebSocketPingWebSocketPongClose constants:
type WebSocketCloseCode int
WebSocketCloseNormalWebSocketCloseGoingAwayWebSocketCloseProtocolErrorWebSocketCloseUnsupportedDataWebSocketClosePolicyViolationWebSocketCloseMessageTooBigWebSocketCloseInternalErrorNumeric close-code values:
| Constant | Value |
|---|---|
WebSocketCloseNormal | 1000 |
WebSocketCloseGoingAway | 1001 |
WebSocketCloseProtocolError | 1002 |
WebSocketCloseUnsupportedData | 1003 |
WebSocketClosePolicyViolation | 1008 |
WebSocketCloseMessageTooBig | 1009 |
WebSocketCloseInternalError | 1011 |
Providers
Section titled “Providers”type Provider interface { Key() string Build(resolver DependencyResolver) (any, error)}
type DependencyResolver interface { Resolve(key string) (any, error)}Application code normally creates providers through the anvil package:
anvil.As, anvil.Named, anvil.Factory, and anvil.NamedFactory. Those
helpers build the stable provider keys that generated code resolves. Custom
providers can implement this interface directly when they need full control.
Plugins
Section titled “Plugins”type Plugin interface { Name() string Register(lifecycle AppLifecycle) error}
type AppLifecycle interface { OnBoot(hook func(ctx context.Context) error) OnShutdown(hook func(ctx context.Context) error) OnError(hook func(ctx context.Context, event ErrorEvent)) ErrorPipeline() ErrorPipeline RegisterProvider(provider Provider) error EventBus() EventBus}
type EventBus interface { Subscribe(topic string, handler func(payload any)) Publish(topic string, payload any)}Plugins return errors from Register for setup failures. Panics are treated as
bugs, not configuration flow.
The core event bus ignores empty topics and nil handlers. Publishing is synchronous on the caller’s goroutine, subscribers run in subscription order, payloads are not cloned, and subscriber panics are not recovered by the bus.