Skip to content

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))
  • you are adding, removing, or reorganizing URL patterns
  • you are changing path parameter extraction or the Param helper
  • 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 Static or StaticFS
  • 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”
  1. router/module.yaml
  2. reference/standard-service/internal/app/routes.go
  3. router/cache.go
Keep it in router when the work is aboutMove out when the work becomes
NewRouter, AddRoute, Group — the structural primitiveshidden route registration, reflection-driven discovery, or service construction
Param extraction from the matched pathroute parameter validation policy or business-rule guards
WithRouteName and named-route URL lookupstdlib-shadow handler aliases or framework-style controller dispatch
Static / StaticFS — small mount primitivesfrontend 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.

import "net/http"
// Shorthand methods: Get, Post, Put, Delete, Patch, Any
app.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))
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 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))
import "github.com/spcent/plumego/router"
// Register with a name
app.AddRoute(http.MethodGet, "/api/users/:id", http.HandlerFunc(handler.GetUser),
router.WithRouteName("user-detail"))
// Reverse to a URL
url := app.URL("user-detail", "id", "42") // → "/api/users/42"
// 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")
// ...
}
import "github.com/spcent/plumego/router"
r := router.NewRouter()
r.Static("/static", "./public") // serve from directory
r.StaticFS("/assets", http.FS(embeddedFS)) // serve from fs.FS
cfg := core.DefaultConfig()
cfg.Router.MethodNotAllowed = true // returns 405 + Allow header instead of 404
app := core.New(cfg, deps)

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.