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.GETsdk.POSTsdk.PUTsdk.PATCHsdk.DELETEsdk.WS- The
Withvariants, such assdk.POSTWith[WritePolicy]
The route field name is the handler method name. Get sdk.GET calls
Get(...); Delete sdk.DELETE calls Delete(...).
Path Syntax
Section titled “Path Syntax”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
/:idand/: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
Section titled “Groups”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:
RequireActorfromAPI.AuditTrailfromV1.- Route policy middleware from
sdk.POSTWith[...]. - Request binding and validation.
- 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.
Route Ordering
Section titled “Route Ordering”The compiler emits deterministic route ordering. Static routes sort before
parameterized routes when needed so /health does not get swallowed by
/:projectId.
WebSocket Routes
Section titled “WebSocket Routes”WebSockets use the WS route marker:
Routes struct { Events sdk.WS `path:"/:projectId/socket"`}The handler accepts sdk.Ctx and sdk.WebSocket.