Routing
Zen uses a custom per-method radix tree router.
It provides :param and *param wildcard segments (or {param} / {path...} syntax
which is auto-converted), trailing-slash redirect, HEAD fallback, 405 detection,
and zero allocations on all routing paths.
Basic Routes
Register handlers with HTTP method and path:
r.GET("/users", listUsers)
r.GET("/users/:id", getUser)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)
You may also use Go ServeMux-style {param} syntax - it is converted to :param
automatically:
r.GET("/users/{id}", getUser) // same as r.GET("/users/:id", getUser)
r.HandleRaw("GET /static/{path...}", fileHandler) // same as /*path
Path Parameters
Access path parameters via c.Param("name"):
r.GET("/users/:id", func(c *zen.Ctx) {
id := c.Param("id")
c.String(200, "User: "+id)
})
Multiple parameters work naturally:
r.GET("/posts/:year/:month/:slug", func(c *zen.Ctx) {
year := c.Param("year")
month := c.Param("month")
slug := c.Param("slug")
c.JSON(200, map[string]string{"year": year, "month": month, "slug": slug})
})
Path parameters are URL-decoded automatically (%2F → /, %C3%A9 → é).
Catch-All (Wildcard) Routes
Use *param (or {param...}) to match everything after the prefix:
r.GET("/static/*path", func(c *zen.Ctx) {
filePath := c.Param("path") // "css/style.css" for /static/css/style.css
})
Catch-all routes must be the final segment. The leading / is stripped from
the captured value (matching Go ServeMux behavior).
Trailing Slash Redirect
When a registered path does not have a trailing slash but the request does (or vice versa), the router returns a 301 redirect automatically:
r.GET("/users", handler) // GET /users/ → 301 → /users
r.GET("/admin/", handler) // GET /admin → 301 → /admin/
HEAD Fallback
If no explicit HEAD route is registered, the router falls back to the GET
handler automatically. An explicit HEAD route takes priority:
r.GET("/users", listUsers) // HEAD /users → falls back to listUsers
r.HEAD("/users", headOnlyHandler) // HEAD /users → uses headOnlyHandler instead
Method Not Allowed
When a path matches a route for a different HTTP method, the router returns
a 405 response with an Allow header listing the available methods:
r.GET("/resource", getHandler)
r.POST("/resource", postHandler)
// PUT /resource → 405, Allow: GET, POST
Serving Static Files
r.File("/", "public/index.html") // single file
r.Static("/images", "assets/images") // directory
r.StaticFS("/assets", assetsFS) // embed.FS
Query Parameters
Query parameters are accessed via the standard Request.URL.Query():
r.GET("/search", func(c *zen.Ctx) {
q := c.Request.URL.Query().Get("q")
page := c.Request.URL.Query().Get("page")
c.String(200, "Search: "+q)
})
Or use c.QueryParam("key") for convenience:
q := c.QueryParam("q")
Request Headers
Access request headers via c.Header("Key"):
r.GET("/protected", func(c *zen.Ctx) {
token := c.Header("Authorization")
if token == "" {
c.Error(401, "missing token")
return
}
c.String(200, "authorized")
})