Skip to content

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, and ErrorPipeline.

Most backends use the first group directly and see the second group only inside generated code.

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"`
}
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.

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.

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.

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.

type HTTPStream interface {
Context() context.Context
Write(data []byte) error
Flush() error
}
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.

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 to 500. Status 500 is internal and sets Expected: false; other valid status codes set Expected: true.
  • NotFound(resource) returns 404 with message "<resource> not found". Empty resource names become "resource".
  • InvalidParam(name, cause) returns 400 with public message invalid request, field message "invalid value", and the technical cause attached. Empty names become "param".
  • Validation().Field(name, message).Err() returns 400 with all collected field messages. Empty field names are ignored. Empty messages become "invalid value".
  • Wrap(cause, operation) returns 500 with public message internal server error, wraps the cause as "<operation>: <cause>", and captures stack frames. Nil causes become "missing cause". Empty operations become "operation".

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() string
func (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 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.

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.

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 Accept returns 406 Not Acceptable.
  • Unknown Content-Type returns 415 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.

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:

  • http for sdk.HTTPTransport
  • graphql for sdk.GraphQLTransport
  • grpc for sdk.GRPCTransport
  • queue for sdk.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.

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
WebSocketMessageUnknown
WebSocketText
WebSocketBinary
WebSocketClose
WebSocketPing
WebSocketPong

Close constants:

type WebSocketCloseCode int
WebSocketCloseNormal
WebSocketCloseGoingAway
WebSocketCloseProtocolError
WebSocketCloseUnsupportedData
WebSocketClosePolicyViolation
WebSocketCloseMessageTooBig
WebSocketCloseInternalError

Numeric close-code values:

ConstantValue
WebSocketCloseNormal1000
WebSocketCloseGoingAway1001
WebSocketCloseProtocolError1002
WebSocketCloseUnsupportedData1003
WebSocketClosePolicyViolation1008
WebSocketCloseMessageTooBig1009
WebSocketCloseInternalError1011
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.

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.