Middleware
Zen middleware has the signature func(*zen.Ctx) - a function that
receives the request context. Call c.Next() to pass control downstream; return
without calling it to short-circuit. Middleware runs in registration order.
Global Middleware
Applied to every route on the router:
r := zen.New(":8080")
r.Use(middleware.Recover)
r.Use(middleware.Logger)
Per-Route (Route-Level) Middleware
Middleware can be applied to a single route. Pass middleware before the final handler - the LAST argument is the endpoint handler, all preceding arguments are per-route middleware:
r.GET("/admin", authMiddleware, adminHandler)
Any number of route-level middleware can be chained:
r.POST("/api/data", validateBody, rateLimit, auditLog, createHandler)
This works with all HTTP method helpers: GET, POST, PUT, DELETE,
PATCH, HEAD, OPTIONS.
The chain is compiled at startup and executes in this order:
Group Middleware → Route Middleware → Handler → Route Middleware → Group Middleware
Route Group Middleware
Applied to all routes in a group (see Route Groups):
admin := r.Group("/admin", authMiddleware)
admin.GET("/dashboard", dashboard)
Group and per-route middleware compose:
admin := r.Group("/admin", auditMiddleware)
admin.GET("/dashboard", rateLimitMiddleware, dashboardHandler)
// Order: audit → rateLimit → handler
Custom Middleware
Write custom middleware to add authentication, logging, rate limiting, or any cross-cutting concern.
Pattern A: Simple Function
func RequestLogger(c *zen.Ctx) {
start := time.Now()
c.Next()
log.Printf("%s %s took %v", c.Request.Method, c.Request.URL.Path, time.Since(start))
}
r.Use(RequestLogger)
Pattern B: With Configuration (Closure)
Return a zen.HandlerFunc from a config function:
type LoggerConfig struct {
Format string
}
func Logger(config LoggerConfig) zen.HandlerFunc {
return func(c *zen.Ctx) {
start := time.Now()
c.Next()
if config.Format == "json" {
slog.LogAttrs(c.Request.Context(), slog.LevelInfo, "request",
slog.String("method", c.Request.Method),
slog.String("path", c.Request.URL.Path),
slog.Duration("duration", time.Since(start)),
)
}
}
}
r.Use(Logger(LoggerConfig{Format: "json"}))
Pattern C: With SkipFunc
Accept an optional zen.SkipFunc to conditionally bypass middleware:
func LoggerWithSkipper(config LoggerConfig, skip zen.SkipFunc) zen.HandlerFunc {
return func(c *zen.Ctx) {
if skip != nil && skip(c.Request) {
c.Next()
return
}
// ... middleware logic ...
c.Next()
}
}
zen.SkipFunc is func(*http.Request) bool. Use the helpers in the auth package:
auth.SkipPaths, auth.SkipPrefixes, auth.SkipMethodsAndPaths.
Passing Data Between Middleware
Use c.Set() and c.Get() to share data across the chain:
func LoadUser(c *zen.Ctx) {
user := fetchUser(c.Request.Header.Get("Authorization"))
c.Set("user", user)
c.Next()
}
func RequireAdmin(c *zen.Ctx) {
user, ok := c.Get("user")
if !ok || !user.(*User).IsAdmin {
c.Error(403, "forbidden")
return
}
c.Next()
}
r.GET("/admin", LoadUser, RequireAdmin, adminHandler)
Error Handling in Middleware
Set error values on the context or respond directly to short-circuit:
func ValidateAPIKey(c *zen.Ctx) {
key := c.Request.Header.Get("X-API-Key")
if !isValid(key) {
c.Error(401, "invalid API key")
return // short-circuits the chain
}
c.Next()
}
func RateLimitMiddleware(c *zen.Ctx) {
if rateLimitExceeded(c.Request) {
c.Response.Header().Set("Retry-After", "60")
c.Error(429, "rate limit exceeded")
return
}
c.Next()
}
Short-Circuiting
Stop the chain by returning without calling c.Next():
func AuthRequired(c *zen.Ctx) {
token := c.Request.Header.Get("Authorization")
if token == "" {
c.Error(401, "unauthorized")
return
}
c.Next()
}
Order of Execution
Middleware runs in registration order. The first middleware registered wraps the next, forming a chain. On each request:
- Middleware pre-processing runs from first registered → last registered
- The handler runs
- Middleware post-processing runs from last registered → first registered
r.Use(middleware.Recover)
r.Use(middleware.Logger)
// For a request, the execution order is:
// Recover (pre) → Logger (pre) → Handler → Logger (post) → Recover (post)
Global, Group, and Per-Route
When all three are present, the execution order is:
Global → Group → Per-Route → Handler → Per-Route → Group → Global
r.Use(authMiddleware) // global
admin := r.Group("/admin", audit) // group
admin.GET("/dashboard", rateLimit, handler) // per-route
// Order: auth → audit → rateLimit → handler
Global middleware runs outermost, then group middleware, then per-route middleware, then the handler - and unwinds in reverse.
Skip Functions
Many middleware accept zen.SkipFunc to bypass processing for specific routes: