Skip to main content

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")
})