Skip to content

Controllers

Controllers are the HTTP entrypoint in Anvil.

type Projects struct {
sdk.Controller `path:"/projects"`
Services *Services
Routes struct {
Create sdk.POST `path:"/"`
List sdk.GET `path:"/"`
Get sdk.GET `path:"/:projectId"`
}
}

The embedded sdk.Controller marks the struct as an HTTP controller. The path tag sets the controller prefix. Routes must resolve to a struct type. Inline route tables are the usual style because the route list stays beside the controller, but a named struct type is accepted as long as the compiler can inspect it. Each exported field in that struct becomes one route, and the field name must match a method on the controller.

Handlers are methods on the controller:

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

Handlers accept one of these shapes:

  • sdk.Ctx
  • sdk.Ctx plus one request model

The request model can be a struct or a pointer to a struct. The compiler reads its tags and emits the binding and validation code before the handler call.

Handlers return:

  • A response body and error
  • Or, for WebSockets, only error

The route field and handler method use the same name:

Routes struct {
Get sdk.GET `path:"/:projectId"`
}
func (p *Projects) Get(ctx sdk.Ctx, req GetProjectRequest) (ProjectDTO, error)

If the field is unexported, missing a path tag, uses a non-route marker, or has no matching handler method, the compiler reports the exact source location and a fix hint.

Controller fields become dependencies when they are tagged with inject:

type Projects struct {
sdk.Controller `path:"/projects"`
Store project.Store `inject:""`
Clock clock.Clock `inject:""`
}

Anvil resolves tagged fields during boot. Untagged exported fields are ignored by DI. If a tagged dependency is missing, ambiguous, or cyclic, wiring fails before traffic is served. Request handling does not use reflection to discover controller dependencies.

Controllers adapt HTTP to application code. Keep transactions, persistence logic, and core business rules in the application layer rather than in the transport edge.

Good controller responsibilities:

  • Call a command or query handler
  • Map domain errors to Anvil errors
  • Set response status or headers
  • Publish an application event