Skip to content

SDK Contract

github.com/TDB-Group/anvil/sdk is the application contract.

Application code imports it for marker types, route table types, request context, middleware interfaces, error contracts, provider contracts, and protocol-neutral stream/socket contracts.

The SDK is the package application code imports for Anvil concepts that appear in route files and handlers. Response bodies, response status codes, framework configuration, cloud clients, logging, and application helpers stay in your application or in the selected driver.

Markers are empty structs. The compiler detects them through Go type identity.

type Projects struct {
sdk.Controller `path:"/projects"`
}
type Services struct {
sdk.Bundle
}
type ReindexProjects struct {
sdk.QueueJob `queue:"projects.reindex"`
}

Markers:

TypePurpose
ControllerDeclares an HTTP Routes table and handler methods.
GroupCreates a nested route tree with shared path and middleware.
BundleMarks a dependency group whose inject-tagged fields are wired.
HttpEndpointLow-level HTTP endpoint marker for scanner extensions.
GrpcEndpointgRPC endpoint marker.
GraphQLEndpointGraphQL endpoint marker.
GraphQLEndpointWith[Policy]GraphQL endpoint marker with an endpoint policy bundle.
QueueJobQueue/background job marker.

The HttpEndpoint and GrpcEndpoint names intentionally use Go-style mixed case preferred by the project, not all-caps acronym naming.

HTTP controllers declare routes in a nested Routes table:

Routes struct {
List sdk.GET `path:"/"`
Create sdk.POST `path:"/"`
Get sdk.GET `path:"/:projectId"`
Update sdk.PUT `path:"/:projectId"`
Patch sdk.PATCH `path:"/:projectId"`
Delete sdk.DELETE `path:"/:projectId"`
Live sdk.WS `path:"/:projectId/live"`
}

Policy variants attach a policy bundle by type:

Routes struct {
Create sdk.POSTWith[WritePolicy] `path:"/"`
Live sdk.WSWith[LivePolicy] `path:"/:projectId/live"`
}

Use[T] marks a policy field as middleware:

type WritePolicy struct {
_ sdk.Use[RequireActor]
_ sdk.Use[AuditTrail]
}

HTTP handlers use:

func (c *Controller) List(ctx sdk.Ctx) ([]ProjectDTO, error)
func (c *Controller) Get(ctx sdk.Ctx, req GetProjectRequest) (ProjectDTO, error)

WebSocket handlers use:

func (c *Controller) Live(ctx sdk.Ctx, socket sdk.WebSocket) error

Request structs are optional. When present, generated code binds fields from param, query, header, and local tags. Exported non-anonymous fields without those binding tags are treated as body fields and decoded through the driver’s codec registry before validation runs.

type Ctx interface {
Context() context.Context
Native() any
Request() HTTPRequest
Response() HTTPResponse
Locals() LocalStore
Errors() ErrorFactory
Next() (any, error)
}

Methods:

  • Context() returns the request context for cancellation, deadlines, tracing, and service calls.
  • Native() returns the driver-owned native request context. Use it only when you intentionally depend on the selected driver.
  • Request() exposes framework-neutral request data.
  • Response() stores response metadata selected by handler code.
  • Locals() stores request-scoped middleware values.
  • Errors() creates Anvil failures with generated route context attached.
  • Next() continues the generated middleware chain.

Next() lives on the context so middleware can call ctx.Next() without a separate function parameter. Normal HTTP handlers return (body, error). Middleware calls Next when it wants the generated chain to continue.

type HTTPRequest interface {
Method() string
Path() string
Param(name string) string
Query(name string) string
Header(name string) string
Body() []byte
Decode(out any) error
}

Generated code handles binding before the handler runs. Use Request() directly for low-level cases such as streaming negotiation, signature verification, raw body inspection, or custom decoding.

type HTTPResponse interface {
Status(code int)
Header(name string, value string)
Stream(handler func(HTTPStream) error) error
}

Handlers return the body as their ordinary return value. Set status, headers, or a stream on ctx.Response() when needed.

type HTTPStream interface {
Context() context.Context
Write(data []byte) error
Flush() error
}

Use ctx.Response().Stream for long-lived HTTP responses such as server-sent events, NDJSON, chunked JSON, and file downloads.

type WebSocket interface {
Context() context.Context
Native() any
Subprotocol() string
Read() (WebSocketMessage, error)
Write(message WebSocketMessage) error
Close(code WebSocketCloseCode, reason string) error
}

Native() follows the same escape-hatch rule as sdk.Ctx.Native(). Use it when direct driver access is worth losing portability.

Message types:

sdk.WebSocketText
sdk.WebSocketBinary
sdk.WebSocketClose
sdk.WebSocketPing
sdk.WebSocketPong

Close codes:

sdk.WebSocketCloseNormal
sdk.WebSocketCloseGoingAway
sdk.WebSocketCloseProtocolError
sdk.WebSocketCloseUnsupportedData
sdk.WebSocketClosePolicyViolation
sdk.WebSocketCloseMessageTooBig
sdk.WebSocketCloseInternalError

Middleware is attached with sdk.Use[T] on a group or policy type. The compiler checks the method signature on T and emits the matching protocol chain. See Callbacks and Middleware for the difference between startup hooks and request-time middleware, then read Middleware for placement rules and execution order.

The method name is the protocol contract. A type with HandleGRPC is gRPC middleware. A type with BeforeHTTP is HTTP middleware. If one type implements both, Anvil can attach it to both protocol-specific chains, but one middleware type per protocol keeps application route groups easier to read.

HandleHTTP, HandleGraphQL, HandleGRPC, and HandleQueue are middleware methods that own the continuation. They decide whether the generated chain continues by calling ctx.Next(). HTTP also exposes BeforeHTTP, OnHTTPError, and AfterHTTP because many HTTP concerns only need one phase of the request. These are request-time middleware methods, not application lifecycle hooks.

HTTP middleware can implement one or more interfaces:

type HTTPMiddleware interface {
HandleHTTP(ctx Ctx) (any, error)
}
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
}

Behavior:

  • HandleHTTP controls the chain manually and calls ctx.Next() to continue.
  • BeforeHTTP runs before the next middleware or handler.
  • AfterHTTP runs after the next middleware or handler and can replace the body or error.
  • OnHTTPError runs only when the next middleware or handler returns an error. Returning nil marks the error as handled.

If a value implements several HTTP middleware interfaces, the driver runs them as one value: BeforeHTTP, then HandleHTTP or automatic continuation, then OnHTTPError if needed, then AfterHTTP.

GraphQL, gRPC, and queue middleware use one continuation method each:

type GraphQLMiddleware interface {
HandleGraphQL(ctx GraphQLCtx) (GraphQLResponse, error)
}
type GRPCMiddleware interface {
HandleGRPC(ctx GRPCCtx) (any, error)
}
type QueueMiddleware interface {
HandleQueue(ctx QueueCtx) error
}

Prefer one middleware type per protocol in application code. A single type can implement several protocol methods when a shared plugin deliberately needs that shape, but protocol-specific types keep route groups readable.

type Provider interface {
Key() string
Build(resolver DependencyResolver) (any, error)
}
type DependencyResolver interface {
Resolve(key string) (any, error)
}

Providers are built while the app is wired, before transports accept requests. Return errors from providers instead of panicking. Use explicit provider keys when two dependencies have the same Go type but different meanings.

type Plugin interface {
Name() string
Register(lifecycle AppLifecycle) error
}

AppLifecycle lets plugins register boot hooks, shutdown hooks, error observers, error mappers, providers, and event bus subscribers. Read Callbacks and Middleware for the runtime order.