Metadata-Driven Routing & Automatic Handler Registration
Routing in Go web applications can quickly become a tangled web of mux.HandleFunc(...)
calls, especially as your API grows. Keeping route paths, handler names, and HTTP methods in sync across documentation and implementation can become a serious source of bugs and drift.
gohandlers eliminates this problem by embracing a metadata-driven approach to routing—automatically generating handler registrations based on your code’s structure and field tags. In this article, we’ll walk through how it works, how to use it, and how it keeps your server and docs perfectly in sync.
🎯 The Problem with Manual Routing
Here’s what typical manual routing might look like:
mux := http.NewServeMux()
mux.HandleFunc("/pets", listPetsHandler)
mux.HandleFunc("/pets/{id}", getPetHandler)
mux.HandleFunc("/pets", createPetHandler) // different method!
Issues this introduces:
- Easy to duplicate or misconfigure paths and methods
- No link between the URL and the handler’s logic or data model
- Hard to discover or document routes
- No enforcement of consistency between handlers and their metadata
✅ The gohandlers Solution
gohandlers generates routing metadata automatically by inspecting:
- Your handler names (e.g.,
GetPet
,ListPets
) - Associated request/response structs (
GetPetRequest
, etc.) - Tags like
route
,query
, andjson
- Optional documentation comments like
// GET /pets/{id}
From this, it generates:
- A function that returns all your handlers as a map
- A YAML file documenting your HTTP surface
- Everything in sync with your real code
🔍 How Metadata is Collected
gohandlers parses your code and builds a model for each handler, including:
- Handler name (e.g.
CreatePet
) - HTTP method (e.g.
POST
) - URL path (e.g.
/pets
) - Reference to the actual Go handler function
This metadata comes from:
Source | What it provides |
---|---|
Function names | Base name of the handler |
Comments (// GET /users ) |
HTTP method and path if explicitly declared |
Tags on struct fields | Parameters used in URL paths or query |
Struct naming (XRequest ) |
Used to infer method and path |
If no method or path is declared, gohandlers uses naming conventions and your binding struct’s route:
tags to derive them automatically.
🗂️ The ListHandlers()
Function
For each group of handlers (e.g., all methods on a struct like *Pets
), gohandlers generates a ListHandlers()
method:
func (p *Pets) ListHandlers() map[string]HandlerInfo {
return map[string]HandlerInfo{
"CreatePet": {
Method: "POST",
Path: "/pets",
Ref: p.CreatePet,
},
"GetPet": {
Method: "GET",
Path: "/pets/{id}",
Ref: p.GetPet,
},
}
}
Each HandlerInfo
struct includes:
Method
: The HTTP method (e.g.GET
,POST
)Path
: The route pattern, with{param}
placeholdersRef
: A reference to the actual handler function (http.HandlerFunc
)
You can now wire up all routes in one consistent loop:
mux := http.NewServeMux()
for _, h := range myHandler.ListHandlers() {
mux.HandleFunc(h.Path, h.Ref)
}
🧠 Automatic Routing Benefits
- No manual route wiring: Paths and handlers stay in sync with your code.
- No route duplication bugs: Each handler is defined only once.
- Cleaner
main.go
: No clutter ofmux.HandleFunc(...)
calls. - Perfect doc generation: Routes and methods are exported to a YAML file (
gh.yml
) for use in documentation or tooling.
📄 YAML Documentation Export
Along with code generation, gohandlers creates a YAML representation of your route metadata:
CreatePet:
method: POST
path: /pets
GetPet:
method: GET
path: /pets/{id}
This file is generated by the yaml
command (triggered alongside list
) and can be used to:
- Generate static documentation
- Drive OpenAPI spec generators
- Share your API structure with frontend or integration teams
✍️ Declaring Metadata with Comments
If you want more control, gohandlers supports in-code comments above your handlers:
// POST /pets
// gh:list
func (p *Pets) CreatePet(...) { ... }
This explicitly tells gohandlers:
- Use
POST
as the HTTP method - Use
/pets
as the path - Include this handler in
ListHandlers()
This is useful when the default inference from naming isn’t sufficient or if you want to override it.
⚙️ Filtering Handlers by Receiver
If your application is organized by service types (e.g., Pets
, Users
, Orders
), you can generate handler lists per receiver using the --recv
flag:
gohandlers list --dir handlers/pets --recv Pets --out list.gh.go
This lets you cleanly group and register handlers on a per-module basis.
🧼 Best Practices
- Use meaningful handler names (
GetPet
,ListOrders
, etc.) - Always tag route fields with
route:"..."
to clarify URL parameters - Use structured comments to document custom routes and methods
- Keep related handlers grouped under receiver structs (e.g.
*Pets
)
🚀 Summary
gohandlers’ metadata-driven routing and automatic registration eliminates the need to manually wire up routes. It:
- Keeps your handlers and routes in sync
- Reduces the potential for errors or duplication
- Provides a clean, declarative routing setup
- Enables easy documentation export via YAML
Whether your API has 5 endpoints or 500, gohandlers ensures every route is consistent, discoverable, and connected to real code.
One source of truth. Zero boilerplate. Fully synchronized.
Happy routing! 🗺️