Routes#

Routes are registered in the routes package in two ways, which work side by side:

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 //.

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.