Skip to content

Routing

HTTP routes are declared through route-table fields.

Routes struct {
Create sdk.POST `path:"/"`
Get sdk.GET `path:"/:projectId"`
Update sdk.PUT `path:"/:projectId"`
Delete sdk.DELETE `path:"/:projectId"`
}

Routes is a special field name on an sdk.Controller. It must resolve to a struct type, and every exported route field must use one of Anvil’s HTTP markers:

  • sdk.GET
  • sdk.POST
  • sdk.PUT
  • sdk.PATCH
  • sdk.DELETE
  • sdk.WS
  • The With variants, such as sdk.POSTWith[WritePolicy]

The route field name is the handler method name. Get sdk.GET calls Get(...); Delete sdk.DELETE calls Delete(...).

Anvil uses :param syntax:

sdk.GET `path:"/:projectId"`

The compiler rejects:

  • {id} path variables
  • Empty parameter names
  • Duplicate parameter names
  • Query strings in route tags
  • URL fragments in route tags
  • Parameter names that are not Go-style identifiers
  • Equivalent collisions such as /:id and /:projectId

The compiler trims and joins controller, group, and route path tags before it validates the final path. A local tag can be projects, /projects, or /projects/; the generated full path is normalized to one leading slash.

Trailing slashes are normalized by the standard HTTP driver for matching, so /projects and /projects/ reach the same route at runtime. At compile time, equivalent declarations still collide so registration stays deterministic.

Groups compose prefixes and shared middleware. A group is a Go struct that embeds sdk.Group. Child controllers or child groups are fields on that struct.

type API struct {
sdk.Group `path:"/api"`
_ sdk.Use[RequireActor]
V1 *V1
}
type V1 struct {
sdk.Group `path:"/v1"`
_ sdk.Use[AuditTrail]
Projects *Projects
}

The sdk.Use[T] field can be named or blank. Blank fields are common because middleware markers are compile-time declarations, not runtime state.

If Projects has path:"/projects", the final path becomes /api/v1/projects.

Middleware inherited from groups is protocol-aware. HTTP middleware applies to HTTP and WebSocket routes. GraphQL, gRPC, and queue middleware apply only to their own protocols even when the endpoints live under the same group prefix.

For the example above, a write HTTP route under Projects runs:

  1. RequireActor from API.
  2. AuditTrail from V1.
  3. Route policy middleware from sdk.POSTWith[...].
  4. Request binding and validation.
  5. The handler.

Groups can nest groups or controllers, but each child can belong to exactly one group tree. The compiler rejects cycles, ambiguous parents, invalid group paths, and fields that look like route children but cannot be resolved.

The manifest records every group with its local path, full path, middleware, children, and source location. That makes group structure visible to OpenAPI, testbeds, built-in analyzer rules, and Starlark rules.

The compiler emits deterministic route ordering. Static routes sort before parameterized routes when needed so /health does not get swallowed by /:projectId.

WebSockets use the WS route marker:

Routes struct {
Events sdk.WS `path:"/:projectId/socket"`
}

The handler accepts sdk.Ctx and sdk.WebSocket.