Skip to main content

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:

  1. Middleware pre-processing runs from first registered → last registered
  2. The handler runs
  3. 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: