Routing
*gas.Router wraps a chi router and adds two
things: every route records the service that owns it, and a handler can declare
its dependencies as parameters (see Handlers and Context).
gas.NewApp creates one router per app and registers it in the container under
*gas.Router, so a service takes it as a constructor parameter. Outside a
service, app.Router() returns the same router.
// Service owns the notes routes. It receives the router from the container,// which NewApp registers under *gas.Router.type Service struct { router *gas.Router}
func New(router *gas.Router) *Service { return &Service{router: router}}
func (s *Service) Name() string { return "notes" }func (s *Service) Close(context.Context) error { return nil }func main() { app := gas.NewApp( gas.WithSingletonService[*Service](New), )
if err := app.Run(); err != nil { log.Fatal(err) }}Registering routes
Section titled “Registering routes”// Init registers the routes. The first argument of every registration is the// owning service, so the router records which service each route belongs to.func (s *Service) Init(context.Context) error { s.router.Handle(s, http.MethodGet, "/notes", s.list)
// The method shorthands call Handle. Get also serves HEAD on the same path. s.router.Get(s, "/notes/{id}", s.show) s.router.Post(s, "/notes", s.create) s.router.Delete(s, "/notes/{id}", s.remove)
return nil}Handle(owner, method, path, handler, middleware...) is the general form.
Get, Head, Post, Put, Patch, and Delete call it with the method
filled in. A route registered for GET, through Get or Handle, also serves
HEAD on the same path.
The handler is either a plain handler (http.HandlerFunc or
func(http.ResponseWriter, *http.Request)) or a DI-aware handler
(func(gas.Context, deps...) error). Anything else panics. On the app’s
router the panic comes from the Handle call itself; inside a Group or
Route callback, which runs when the router builds its tree, it comes from
Start for a block registered before Start.
Handlers and Context covers both forms.
The trailing arguments are per-route middleware, gas.MiddlewareByName(...) or
gas.MiddlewareFunc(...); see Middleware.
When registrations take effect
Section titled “When registrations take effect”The router collects registrations and builds its chi tree when the app starts:
Start seals the router after every singleton service has run Init. Until
then, the order of Use and Handle calls on the app’s router does not
matter, so services can add middleware with Use and register routes in any
order during Init. Named middleware is the exception: a name must be
registered before a call refers to it (see
Named middleware). Global middleware
is always applied before the routes, which is the order chi requires.
A registration made after Start takes effect immediately. The router builds a
fresh tree from everything registered so far and swaps it in atomically;
requests already in flight finish on the previous tree.
Paths and parameters
Section titled “Paths and parameters”Patterns use chi’s syntax: {name} matches one path segment,
{name:regexp} adds a regular expression, and a trailing * matches the rest
of the path.
// Path parameters use chi's pattern syntax. A parameter can carry a regular// expression, and a trailing * matches the rest of the path.s.router.Get(s, "/notes/{id}/revisions/{rev:[0-9]+}", s.revision)s.router.Get(s, "/files/*", s.file)
func (s *Site) revision(ctx gas.Context) error { id := ctx.Param("id") rev := ctx.Param("rev") return ctx.JSON(http.StatusOK, map[string]string{"id": id, "rev": rev})}
func (s *Site) file(ctx gas.Context) error { rest := ctx.Param("*") // everything after /files/ return ctx.Text(http.StatusOK, rest)}ctx.Param(name) returns a path parameter, and ctx.Param("*") the part
matched by the wildcard. A plain handler reads the same values with
chi.URLParam:
// A plain handler reads path parameters with chi directly.func plainShow(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte(id))}Groups and prefixes
Section titled “Groups and prefixes”Group scopes middleware to a set of routes without changing their paths:
// Group scopes middleware to a set of routes without changing their paths.s.router.Group(func(sub *gas.Router) { sub.UseMiddlewareByName("require-auth") sub.Get(s, "/admin/dashboard", s.list) sub.Get(s, "/admin/settings", s.list)})Route mounts a path prefix. Several services may mount the same prefix, which
is how independent features share /api:
// Route mounts a path prefix. Several services can call it with the same// pattern; later calls attach to the mount the first one created.func (s *Notes) Init(context.Context) error { s.router.Route("/api", func(sub *gas.Router) { sub.Use(gas.MiddlewareByName("require-auth")) sub.Get(s, "/notes", listNotes) // GET /api/notes, guarded }) return nil}
// Billing mounts the same prefix. Its block is unaffected by the Use above.func (s *Billing) Init(context.Context) error { s.router.Route("/api", func(sub *gas.Router) { sub.Get(s, "/plans", listPlans) // GET /api/plans, not guarded }) return nil}Calling Route again with a pattern that is already mounted attaches to the
existing mount instead of failing. Each Route body runs in its own group, so
Use inside one block applies only to the handlers registered in that block.
The second block’s routes above are not behind require-auth, even though both
hang off /api.
Route nests, and the prefixes accumulate:
// Route nests. Prefixes accumulate, so this registers GET /api/v1/notes.s.router.Route("/api", func(api *gas.Router) { api.Route("/v1", func(v1 *gas.Router) { v1.Get(s, "/notes", s.list) })})Serving an http.Handler
Section titled “Serving an http.Handler”Handle accepts functions, not http.Handler values. Pass the handler’s
ServeHTTP method, which has the plain handler signature:
// Handle accepts functions only. To serve an http.Handler, pass its ServeHTTP// method, which has the plain handler signature.files := http.StripPrefix("/static/", http.FileServer(http.Dir("public")))s.router.Get(s, "/static/*", files.ServeHTTP)Not found and method not allowed
Section titled “Not found and method not allowed”A request that matches no route gets chi’s default response: 404 with the
plain-text body 404 page not found. A request whose path matches but whose
method does not gets 405 with an empty body and an Allow header listing
the methods the path serves, including the implicit HEAD of a GET route
(GET and HEAD for a path with only a GET route). The methods are listed
in no fixed order.
NotFound(owner, handler) replaces the 404 handler. It takes either handler
form, and a DI-aware not-found handler has its dependencies checked at startup
like any route.
// NotFound replaces chi's default 404. A router accepts one not-found handler;// a second call panics.s.router.NotFound(s, func(ctx gas.Context) error { return gas.NotFound("no route for " + ctx.Request().URL.Path)})A router accepts one not-found handler. A second NotFound call panics, even
from the same service.
Ownership
Section titled “Ownership”The first argument of Handle, Get, NotFound, and the other registration
methods is the owning service. The router records each route under the owner’s
Name(). Passing nil makes the router itself the owner: those routes are
recorded under the name gas/router, including routes registered inside a
Group or Route block.
Ownership is what the kill switch acts on. CloseService replaces every route
the closed service owns with a 503 handler and disables the named middleware
it registered wherever that middleware is used; RestartService brings them
back. See Ownership and teardown.
Routes() returns a snapshot of the registered routes, grouped by owner name.
It leaves out the implicit HEAD routes that GET registrations add.
Each route lists the named middleware that applies to it, with one exception: a
route registered directly on the app’s router before a global
Use of a named middleware is listed without that name, even though the
middleware runs on it. This is tracked as KI-056 in
Known issues.
NamedMiddleware() maps each registered middleware name to the name of the
service that owns it.
// Routes reports every registered route grouped by the name of its owner.func printRoutes(router *gas.Router) { for owner, routes := range router.Routes() { for _, rt := range routes { log.Printf("%s: %s %s %v", owner, rt.Method, rt.Path, rt.Middleware) } }}App.Run logs both right after Start, when at least one route is
registered. When the environment is development, it logs one table of routes
and named middleware; otherwise it logs one line per route and one per named
middleware. With no routes registered it logs nothing, not even the named
middleware. When nothing sets the
environment, for example when no ConfigProvider is registered, it is empty and
the per-line form is used. Calling Start and Serve yourself logs no route map.
The underlying chi router
Section titled “The underlying chi router”Mux() returns the most recently built chi tree. Changes made to it directly
are not recorded by the router, so they are lost the next time the tree is
rebuilt: Start builds a fresh tree, and so does every registration or
teardown after it. Register routes and middleware through the *gas.Router
methods instead.