Router Primer
Router Primer
Section titled “Router Primer”Open this page after Stable Roots when the change still belongs to the default service path and the real question has narrowed to how the service maps URLs to handlers: which path wins, how params are extracted, and how named routes are reversed.
router owns HTTP route matching, path parameter extraction, route groups, and reverse routing. It is intentionally narrow: it knows nothing about JSON responses, auth decisions, or application construction.
app.Get("/api/v1/items", http.HandlerFunc(items.List))app.Post("/api/v1/items", http.HandlerFunc(items.Create))
grp := app.Group("/api/v1/users")grp.Get("/:id", http.HandlerFunc(users.Get))Start here when
Section titled “Start here when”- you are adding, removing, or reorganizing URL patterns
- you are changing path parameter extraction or the
Paramhelper - you are building a route group with a shared prefix or middleware
- you are resolving a named route URL with
WithRouteName - you are mounting a static file tree with
StaticorStaticFS
Do not start here when
Section titled “Do not start here when”- the change is about response shaping or error formatting — start from
contract - the change introduces auth validation or security headers — start from
security - the work is about how the app assembles and starts — start from
core - the change adds tenant-specific route factories, feature-flag routing, or frontend cache policy
First files to read in the current repository
Section titled “First files to read in the current repository”router/module.yamlreference/standard-service/internal/app/routes.gorouter/cache.go
Concrete ownership examples
Section titled “Concrete ownership examples”Keep it in router when the work is about | Move out when the work becomes |
|---|---|
NewRouter, AddRoute, Group — the structural primitives | hidden route registration, reflection-driven discovery, or service construction |
Param extraction from the matched path | route parameter validation policy or business-rule guards |
WithRouteName and named-route URL lookup | stdlib-shadow handler aliases or framework-style controller dispatch |
Static / StaticFS — small mount primitives | frontend asset policy, SPA fallback, cache headers, or ETag generation |
Routes are registered through core.App (the typical path) or directly on router.Router when you need a standalone router.
Register routes via core.App
Section titled “Register routes via core.App”import "net/http"
// Shorthand methods: Get, Post, Put, Delete, Patch, Anyapp.Get("/api/users", http.HandlerFunc(handler.ListUsers))app.Post("/api/users", http.HandlerFunc(handler.CreateUser))app.Put("/api/users/:id", http.HandlerFunc(handler.UpdateUser))app.Delete("/api/users/:id", http.HandlerFunc(handler.DeleteUser))Read path parameters
Section titled “Read path parameters”import "github.com/spcent/plumego/router"
func (h UserHandler) Get(w http.ResponseWriter, r *http.Request) { id := router.Param(r, "id") // extracts :id from matched path // ...}Route groups (shared prefix)
Section titled “Route groups (shared prefix)”Route groups work directly on router.Router. Access the underlying router from core.App when you need groups, or use AddRoute with full paths:
import "github.com/spcent/plumego/router"
r := router.NewRouter()
api := r.Group("/api/v1")api.AddRoute("GET", "/users", http.HandlerFunc(handler.ListUsers))api.AddRoute("POST", "/users", http.HandlerFunc(handler.CreateUser))api.AddRoute("GET", "/users/:id", http.HandlerFunc(handler.GetUser))api.AddRoute("DELETE", "/users/:id", http.HandlerFunc(handler.DeleteUser))
admin := r.Group("/admin")admin.AddRoute("GET", "/stats", http.HandlerFunc(handler.Stats))Named routes and reverse lookup
Section titled “Named routes and reverse lookup”import "github.com/spcent/plumego/router"
// Register with a nameapp.AddRoute(http.MethodGet, "/api/users/:id", http.HandlerFunc(handler.GetUser), router.WithRouteName("user-detail"))
// Reverse to a URLurl := app.URL("user-detail", "id", "42") // → "/api/users/42"Wildcard segments
Section titled “Wildcard segments”// Matches /files/images/logo.png → filepath = "images/logo.png"app.Get("/files/*filepath", http.HandlerFunc(handler.ServeFile))
func (h FileHandler) Serve(w http.ResponseWriter, r *http.Request) { path := router.Param(r, "filepath") // ...}Static file serving
Section titled “Static file serving”import "github.com/spcent/plumego/router"
r := router.NewRouter()r.Static("/static", "./public") // serve from directoryr.StaticFS("/assets", http.FS(embeddedFS)) // serve from fs.FS405 Method Not Allowed
Section titled “405 Method Not Allowed”cfg := core.DefaultConfig()cfg.Router.MethodNotAllowed = true // returns 405 + Allow header instead of 404app := core.New(cfg, deps)Why this primer exists
Section titled “Why this primer exists”Route registration is the entry-point map for every request the service will ever receive. If routes are hard to grep, a reader cannot trace a request path. router enforces the rule that registration stays explicit and linear: one method, one path, one handler. The moment routing learns about feature flags, tenant IDs, or plugin catalogs, that constraint is gone.
Read next
Section titled “Read next”- router API Quick Reference — complete function signatures, path syntax, and static serving
- Core Primer
- Contract Primer
- Reference App
- Repository Boundaries