Route requests
The router is chi underneath, with two additions: every route records the service that owns it, and handlers may declare their dependencies as parameters.
Registering routes
Section titled “Registering routes”// Handle takes the owning service name, a method, a path, and either a plain// http.HandlerFunc or a DI-aware handler. Both forms coexist on one router.func (s *Service) Init() error { s.router.Handle(s.Name(), http.MethodGet, "/notes", s.list)
s.router.Handle(s.Name(), http.MethodPost, "/notes", s.create, gas.MiddlewareByName("require-auth"), )
s.router.Handle(s.Name(), http.MethodGet, "/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
return nil}The first argument is the owning service name, which is what makes
teardown and route attribution possible. Passing
s.Name() keeps it honest.
Middleware is attached per route with gas.MiddlewareByName(...), resolved from
the router’s registry, or gas.MiddlewareFunc(...) for an inline one.
Handler forms
Section titled “Handler forms”Both work on the same router. A plain http.HandlerFunc is passed through
untouched. A DI-aware handler takes gas.Context first, dependencies in the
middle, and returns error:
func (s *Service) createUser(ctx gas.Context, db gas.DatabaseProvider, mailer gas.EmailProvider) errorEach dependency is resolved from that request’s scope. At startup the router checks that every declared parameter is registered, so a missing dependency fails before the server accepts traffic rather than on the first request that hits the route.
Returning an error renders it through the error handler, and panics are recovered and routed the same way.
Grouping
Section titled “Grouping”Group scopes middleware without changing paths:
// Group scopes middleware to a set of routes without changing their paths.func groups(router *gas.Router, s *Service) { router.Group(func(sub *gas.Router) { sub.UseMiddlewareByName("require-auth") sub.Handle("admin", http.MethodGet, "/admin/dashboard", s.list) sub.Handle("admin", http.MethodGet, "/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 mounts(router *gas.Router, s *Service) { router.Route("/api", func(sub *gas.Router) { sub.Use(gas.MiddlewareByName("require-auth")) sub.Handle("notes", http.MethodGet, "/notes", s.list) // guarded })
// Registered by a different service, same mount, unaffected by the Use above. router.Route("/api", func(sub *gas.Router) { sub.Handle("billing", http.MethodGet, "/plans", s.list) // not guarded })}Reading the request
Section titled “Reading the request”func (s *Service) show(ctx gas.Context) error { id := ctx.Param("id") // path parameter verbose := ctx.Query("verbose") // query string trace := ctx.Header("X-Trace-Id") // request header
ctx.SetHeader("Cache-Control", "no-store") return ctx.JSON(http.StatusOK, map[string]string{"id": id, "v": verbose, "t": trace})}gas.Context embeds context.Context, so it goes straight into database
calls, gRPC clients, and anything else taking a context, with no unwrapping.
It also carries the response helpers: JSON, XML, RSS, HTML, Text,
NoContent, Redirect, Error, and ErrorJSON, plus BindJSON and
BindForm for request bodies. The full method list is on
pkg.go.dev.
Because it is an interface, tests can implement only the parts they need by
embedding gas.Context in a struct and overriding a method or two.
Escape hatches
Section titled “Escape hatches”router.NotFound(service, handler) sets the fallback. router.Mux() returns
the underlying chi router when you need something Gas does not wrap.
router.Routes() reports every registered route with its owner, and
router.NamedMiddleware() does the same for middleware, which is what the
startup route-map log prints in development.