Routes#
Routes are registered in the routes package in two ways, which work side by side:
- Generated routes. A handler declares its route with an
//anar:routecomment, andanar routeswritesroutes/generated_routes.go. This is how CRUD handlers made byanar handlers/anar scaffoldget their routes. - Hand-written routes in
registerRoutesinroutes/roots.go, such as login, registration and OAuth.
Files#
| File | Who edits it | Purpose |
|---|---|---|
routes/roots.go |
You | SetupRoutes, which mounts one group per API version (and the unversioned alias), and registerRoutes, which holds the hand-written routes every version serves. |
routes/middleware.go |
You (rarely) | The middlewares set generated routes use (JWT, permissions and cache). |
routes/generated_routes.go |
anar routes only |
Generated from the directives. Never edit it; it is overwritten. |
Declaring a route#
Put one or more directives directly above a handler method in handlers/:
//anar:route GET /products perm=view
func (h Handler) GetAllProducts(c *gin.Context) { ... }
//anar:route POST /contact auth=public
func (h Handler) Contact(c *gin.Context) { ... }
//anar:route GET /reports/:id perm="group:admin OR view_reports"
func (h Handler) GetReport(c *gin.Context) { ... }
Format: //anar:route METHOD PATH [options], with no space after //.
METHOD:GET,POST,PUT,PATCHorDELETE.PATH: relative to/api. Lower-case kebab-case segments,:paramparameters, and an optional final*wildcard.
| Option | Default | Meaning |
|---|---|---|
auth=jwt / auth=public |
jwt |
Require a valid access token, or allow anyone. |
perm=<expression> |
none | Also require a permission, using the same expressions as RequirePermissionsString: permission codes, group:<name>, AND, OR, XOR, NOT, parentheses. Quote it if it has spaces. Users with admin: true always pass. |
name=<identifier> |
handler name | Name shown in anar routes:list. |
cache=<duration> |
none | Cache successful responses for this long, from 1s to 24h. GET routes only. |
tags=<a,b> |
the route's resource | Resources the cached response depends on, for invalidation. Needs cache=. |
version=<N> |
1 |
First API version that serves the route. See API Versioning. |
until=<M> |
none | Last API version that serves the route. |
Routes are protected by default: without auth=public, a route requires a token. A handler that should deliberately have no route gets //anar:noroute, which stops anar routes from warning about it.
Generated CRUD routes#
anar handlers and anar scaffold write these directives for each model, then run anar routes:
| Handler | Route | Permission |
|---|---|---|
GetAll<Plural> |
GET /api/v1/<plural> |
view |
Get<Model>ByID |
GET /api/v1/<plural>/:id |
view |
Add<Model> |
POST /api/v1/<plural> |
create |
Update<Model> |
PUT /api/v1/<plural>/:id (applies the fields sent) |
update |
Delete<Model> |
DELETE /api/v1/<plural>/:id |
delete |
GetAll<Plural>Simple |
none (//anar:noroute; unpaginated) |
<plural> is the kebab-case plural of the model: OrderItem gives /api/v1/order-items, Category gives /api/v1/categories. The view, create, update and delete permissions and the read, write and read_write groups exist in every new project. Grant them to users through /api/v1/permissions, or edit perm= on a handler to use your own.
To make a route public, change its directive (for example //anar:route GET /products auth=public) and run anar routes.
API Versioning#
The API is versioned in the URL. Routes are served under /api/v1, and each new version you declare gets its own prefix (/api/v2, …). A route's directive says which versions serve it:
| Option | Meaning |
|---|---|
version=N |
The first version that serves the route. The default is 1. |
until=M |
The last version that serves it. It is gone from version M+1 on. |
Each version serves every route active in it, so a new version inherits everything from the one before. To change an endpoint in v2, add a handler for the same method and path with version=2. It replaces the old one from v2 on, while v1 keeps the original:
//anar:route GET /products perm=view
func (h Handler) GetAllProducts(c *gin.Context) { ... } // v1
//anar:route GET /products perm=view version=2
func (h Handler) GetAllProductsV2(c *gin.Context) { ... } // v2 and later
To drop an endpoint from v2, mark it until=1. The versions are 1 through the highest version=, or one past the highest until=. anar routes writes this list into generated_routes.go as generatedVersions, and SetupRoutes mounts one group per version. Hand-written routes in registerRoutes are mounted on every version. anar routes:list, anar openapi and the conflict checks all work per version.
Unversioned paths. Projects created before versioning served everything at /api/.... While API_LEGACY_ROUTES is on (the default), those paths still serve version 1, with two extra headers telling clients where to go:
Deprecation: true
Link: </api/v1/products>; rel="successor-version"
Move your clients to /api/v1, then set API_LEGACY_ROUTES=false. Links the server sends (email confirmation, password reset, provider linking) already use /api/v1. While the setting is on, the default OAuth callback URL stays at /api/auth/oauth/<name>/callback, so URLs already registered at providers keep working. Check them before turning it off.
Older projects. A project whose SetupRoutes still calls registerGeneratedRoutes(api, handler, mw), with three arguments, keeps unversioned routes, and anar routes rejects version= and until= there. To adopt versioning, copy the apiPrefix constant, SetupRoutes and registerRoutes layout from routes/roots.go in anar-core v0.11.0, then run anar routes.
Commands#
| Command | What it does |
|---|---|
anar routes |
Check every directive and regenerate routes/generated_routes.go. Errors show file and line. |
anar routes --check |
Change nothing; fail if the generated file is out of date or there are errors. Add --strict to also fail on handlers that have no route. Use it in CI. |
anar routes:list |
List all routes, generated and hand-written. |
anar routes --annotate [--write] |
Add directives to existing CRUD handlers that match the generated names. Dry run without --write. |
anar routes --install [--write] |
Set up generated routes in a project created before they existed. Dry run without --write. |
anar routes refuses to generate when a route would clash: the same method and path in two places (generated or hand-written), or the same path with different parameter names, which Gin rejects at startup.
Adding a hand-written route#
For routes that need custom middleware or don't belong to a handler method, add them to registerRoutes in routes/roots.go. Every API version serves them:
api.GET("/health", handler.Health) // public
protected.POST("/reports/rebuild", handler.RebuildReports) // requires a token
anar routes reads these too, so a clash with a generated route is reported.
Projects created before generated routes#
anar routes --install --annotate # show what would change
anar routes --install --annotate --write # apply it and generate the routes
--install adds routes/middleware.go and the registerGeneratedRoutes(...) call. --annotate adds directives only to handlers whose names exactly match the generated CRUD handlers of your models. Any hand-written route you already added for those handlers is reported as a clash; delete either the hand-written line or the directive.