Documentation Index
Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
Middleware wraps handlers to add behavior before or after request processing. Theyβre perfect for cross-cutting concerns like logging, authentication, and error recovery that apply to multiple routes.
What is middleware?
Middleware sits between the incoming request and your handler. It can:
- Run code before the handler (check auth, log start time)
- Run code after the handler (log duration, add headers)
- Short-circuit the request (reject unauthorized users)
- Modify the request or response
Think of middleware as layers of an onion. Each request passes through each layer going in, hits your handler, then passes through each layer coming out.
The middleware signature
A middleware is a function that takes a handler and returns a new handler:
type Middleware func(Handler) Handler
Hereβs a simple logging middleware:
func timing() mizu.Middleware {
return func(next mizu.Handler) mizu.Handler {
return func(c *mizu.Ctx) error {
// BEFORE: runs before the handler
start := time.Now()
// Call the next handler in the chain
err := next(c)
// AFTER: runs after the handler
c.Logger().Info("request completed",
"path", c.Request().URL.Path,
"duration", time.Since(start),
)
return err
}
}
}
Applying middleware
Global middleware
Apply to all routes with app.Use():
app := mizu.New()
// These run for every request, in order
app.Use(timing())
app.Use(cors())
app.Get("/", handler)
app.Get("/users", listUsers)
Scoped middleware
Apply to specific routes with app.With():
app := mizu.New()
// Create a router with auth middleware
protected := app.With(requireAuth())
// Only these routes require auth
protected.Get("/profile", getProfile)
protected.Post("/settings", updateSettings)
// This route has no auth requirement
app.Get("/public", publicHandler)
Group middleware
Apply to a group of routes:
app.Group("/api", func(r *mizu.Router) {
// All routes in this group get this middleware
r.Use(apiKeyRequired())
r.Get("/users", listUsers)
r.Post("/users", createUser)
})
Execution order
Middleware runs in the order you add them. For the chain A β B β C β handler:
app.Use(A()) // First added
app.Use(B()) // Second added
app.Use(C()) // Third added
app.Get("/", handler)
Request flow:
A before β B before β C before β handler
handler returns
C after β B after β A after
Request β A β B β C β Handler
β
Response β A β B β C β (result)
Built-in middleware
Mizu includes one middleware by default:
// Logger middleware is added automatically by mizu.New()
app := mizu.New() // Includes request logging
The Logger middleware logs each request with method, path, status, and duration. Configure it:
app.Use(mizu.Logger(mizu.LoggerOptions{
Mode: mizu.Dev, // Dev (text) or Prod (JSON)
Color: true, // Colored terminal output
UserAgent: true, // Include User-Agent in logs
}))
Writing middleware
Basic middleware
func myMiddleware() mizu.Middleware {
return func(next mizu.Handler) mizu.Handler {
return func(c *mizu.Ctx) error {
// Your logic here
return next(c)
}
}
}
Authentication middleware
func requireAuth() mizu.Middleware {
return func(next mizu.Handler) mizu.Handler {
return func(c *mizu.Ctx) error {
token := c.Request().Header.Get("Authorization")
if token == "" {
return c.JSON(401, map[string]string{
"error": "missing authorization header",
})
}
// Validate token...
user, err := validateToken(token)
if err != nil {
return c.JSON(401, map[string]string{
"error": "invalid token",
})
}
// Store user in context for handlers
ctx := context.WithValue(c.Context(), userKey{}, user)
*c.Request() = *c.Request().WithContext(ctx)
return next(c)
}
}
}
CORS middleware
func cors() mizu.Middleware {
return func(next mizu.Handler) mizu.Handler {
return func(c *mizu.Ctx) error {
c.Header().Set("Access-Control-Allow-Origin", "*")
c.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
c.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// Handle preflight requests
if c.Request().Method == "OPTIONS" {
return c.NoContent()
}
return next(c)
}
}
}
Recovery middleware
func recover() mizu.Middleware {
return func(next mizu.Handler) mizu.Handler {
return func(c *mizu.Ctx) error {
defer func() {
if r := recover(); r != nil {
c.Logger().Error("panic recovered", "error", r)
c.JSON(500, map[string]string{"error": "internal server error"})
}
}()
return next(c)
}
}
}
Note: Mizu has built-in panic recovery that passes panics to your error handler. This example shows the pattern.
Middleware with configuration
Use the options pattern for configurable middleware:
type RateLimitOptions struct {
Requests int
Window time.Duration
KeyFunc func(*mizu.Ctx) string
}
func rateLimit(opts RateLimitOptions) mizu.Middleware {
// Set defaults
if opts.Requests == 0 {
opts.Requests = 100
}
if opts.Window == 0 {
opts.Window = time.Minute
}
if opts.KeyFunc == nil {
opts.KeyFunc = func(c *mizu.Ctx) string {
return c.Request().RemoteAddr
}
}
// Your rate limiting logic here...
limiter := newLimiter(opts)
return func(next mizu.Handler) mizu.Handler {
return func(c *mizu.Ctx) error {
key := opts.KeyFunc(c)
if !limiter.Allow(key) {
return c.JSON(429, map[string]string{
"error": "too many requests",
})
}
return next(c)
}
}
}
Usage:
app.Use(rateLimit(RateLimitOptions{
Requests: 60,
Window: time.Minute,
}))
Passing data between middleware
Use Goβs context to pass data from middleware to handlers:
// Define a unique key type
type userKey struct{}
// Middleware: store user in context
func userLoader() mizu.Middleware {
return func(next mizu.Handler) mizu.Handler {
return func(c *mizu.Ctx) error {
user := loadUserFromToken(c)
ctx := context.WithValue(c.Context(), userKey{}, user)
*c.Request() = *c.Request().WithContext(ctx)
return next(c)
}
}
}
// Helper: retrieve user from context
func getUser(c *mizu.Ctx) *User {
user, _ := c.Context().Value(userKey{}).(*User)
return user
}
// Handler: use the user
func profile(c *mizu.Ctx) error {
user := getUser(c)
return c.JSON(200, user)
}
Using net/http middleware
Mizu can use standard net/http middleware via Compat.Use():
// Standard net/http middleware signature
func standardMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ... your logic
next.ServeHTTP(w, r)
})
}
// Use it with Mizu
app := mizu.New()
app.Compat.Use(standardMiddleware)
This lets you use middleware from other Go libraries that follow the standard pattern.
Common patterns
Skip paths
Donβt run middleware on certain paths:
func skipPaths(skip []string, mw mizu.Middleware) mizu.Middleware {
skipSet := make(map[string]bool)
for _, p := range skip {
skipSet[p] = true
}
return func(next mizu.Handler) mizu.Handler {
return func(c *mizu.Ctx) error {
if skipSet[c.Request().URL.Path] {
return next(c) // Skip middleware
}
return mw(next)(c) // Apply middleware
}
}
}
// Usage: skip auth for public paths
app.Use(skipPaths([]string{"/health", "/public"}, requireAuth()))
Conditional middleware
Apply middleware based on request:
func onlyJSON(mw mizu.Middleware) mizu.Middleware {
return func(next mizu.Handler) mizu.Handler {
return func(c *mizu.Ctx) error {
ct := c.Request().Header.Get("Content-Type")
if strings.HasPrefix(ct, "application/json") {
return mw(next)(c)
}
return next(c)
}
}
}
Summary
| Concept | Description |
|---|
| Signature | func(Handler) Handler |
| Global | app.Use(middleware) |
| Scoped | app.With(middleware) |
| Group | Inside app.Group() callback |
| Order | First added runs first |
| net/http compat | app.Compat.Use() |
Middleware keeps your handlers focused on business logic while handling cross-cutting concerns in reusable, composable functions.