Middleware
Middleware in MuxMaster is any function with the signature:
func(http.Handler) http.Handler
This is the same signature used by net/http, chi, gorilla/mux, and most other Go web libraries, so any existing middleware is compatible with MuxMaster without modification.
Table of Contents
- How Middleware Works
- Global Middleware
- Pre-Routing Middleware
- Group Middleware
- Per-Route Middleware with With
- Writing Custom Middleware
- Built-in Middleware Reference
How Middleware Works
MuxMaster applies middleware at route registration time. When you call mux.Use(mw) and then mux.GET("/path", handler), the handler stored in the router is mw(handler) — not the original handler plus a middleware list.
This means:
- Zero per-request overhead from iterating a middleware chain
- Middleware applied to a route stays with that route, regardless of later
Usecalls Usemust be called before the routes it should affect
Execution order mirrors nesting order: the first middleware listed in Use is the outermost wrapper (runs first on request, last on response).
mux.Use(A)
mux.Use(B)
mux.GET("/path", handler)
// Execution: A → B → handler → B → A
Global Middleware
Use appends middleware to the mux's global chain. It applies to all routes registered after the call:
mux := muxmaster.New()
mux.Use(middleware.Logger(os.Stdout))
mux.Use(middleware.Recoverer)
mux.GET("/api/users", listUsers) // wrapped by Logger and Recoverer
Pre-Routing Middleware
Pre registers middleware that runs before the router matches the request. Use it to rewrite or normalize the URL before the radix tree sees it.
mux.Pre(middleware.CleanPath)
mux.Pre(middleware.StripSlashes)
Pre-routing middleware cannot access path parameters because routing has not happened yet. It is useful for path normalization, request ID injection, and real IP extraction.
Group Middleware
Middleware registered on a group applies only to the routes in that group, after any mux-level middleware:
mux := muxmaster.New()
mux.Use(middleware.Logger(os.Stdout)) // runs for all routes
api := mux.Group("/api/v1")
api.Use(requireAPIKey) // runs only for routes in /api/v1
api.GET("/users", listUsers) // Logger → requireAPIKey → listUsers
mux.GET("/health", health) // Logger → health (no requireAPIKey)
Per-Route Middleware with With
With returns a copy of the mux or group with additional middleware scoped to the next route registration:
// On the mux
mux.With(requireAdmin).DELETE("/users/:id", deleteUser)
// On a group
api.With(rateLimit, auditLog).POST("/payments", processPayment)
With does not mutate the original mux or group; it returns a new, temporary scope.
Writing Custom Middleware
A middleware is a function that receives the next handler and returns a new handler:
func requireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !isValidToken(token) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
mux.Use(requireAuth)
Passing configuration to middleware
Wrap the middleware function in a constructor that accepts options:
func RateLimit(requestsPerSecond int) func(http.Handler) http.Handler {
limiter := rate.NewLimiter(rate.Limit(requestsPerSecond), requestsPerSecond)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
mux.Use(RateLimit(100))
Sharing data between middleware and handlers via context
Use context.WithValue with an unexported key type to avoid collisions:
type ctxKey string
const userIDKey ctxKey = "userID"
func injectUserID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := extractUserIDFromToken(r.Header.Get("Authorization"))
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// In a handler:
userID := r.Context().Value(userIDKey).(string)
Alternatively, use middleware.WithValue for simple cases:
mux.Use(middleware.WithValue("requestEnv", "production"))
// In a handler:
env := r.Context().Value("requestEnv").(string)
Built-in Middleware Reference
Import the middleware sub-package:
import "github.com/FlavioCFOliveira/MuxMaster/middleware"
Logger
Logs each request after it completes. Output format: timestamp method path status duration.
mux.Use(middleware.Logger(os.Stdout))
Sample output:
2026-04-17T10:05:31Z GET /api/v1/users 200 1.243ms
2026-04-17T10:05:32Z POST /api/v1/users 201 4.871ms
Parameters:
out io.Writer— destination for log lines; panics if nil
Recoverer
Catches panics in downstream handlers, writes a 500 response, and resumes normal request processing. Without this middleware a panic crashes the entire server.
mux.Use(middleware.Recoverer)
CORS
Handles Cross-Origin Resource Sharing. Responds to preflight OPTIONS requests and sets the appropriate CORS headers on responses.
mux.Use(middleware.CORS(middleware.CORSOptions{
AllowedOrigins: []string{"https://app.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 86400, // seconds to cache preflight response
}))
To allow all origins (not recommended for authenticated APIs):
mux.Use(middleware.CORS(middleware.CORSOptions{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST"},
}))
CORSOptions fields:
| Field | Type | Description |
|---|---|---|
AllowedOrigins |
[]string |
Origins that may access the resource |
AllowedMethods |
[]string |
HTTP methods allowed in the actual request |
AllowedHeaders |
[]string |
Request headers that may be used |
ExposedHeaders |
[]string |
Response headers accessible to the browser |
AllowCredentials |
bool |
Whether the response can include cookies (cannot use "*") |
MaxAge |
int |
Seconds to cache the preflight response |
BasicAuth
Requires HTTP Basic Authentication credentials for the wrapped route or group.
credentials := map[string]string{
"admin": "secret",
"reader": "readonly",
}
mux.Use(middleware.BasicAuth("My API", credentials))
Parameters:
realm string— shown to the user in the browser's credential promptcredentials map[string]string— map of username → password
Compress
Compresses responses using gzip or deflate, depending on the Accept-Encoding header.
mux.Use(middleware.Compress(5)) // compression level 1–9; 5 is a good default
Responses smaller than a threshold are not compressed. The Content-Encoding: gzip header is set automatically.
ThrottleBacklog
Limits the number of concurrently executing handlers. Requests that exceed the limit are queued; requests that exceed the queue are rejected with 503.
mux.Use(middleware.ThrottleBacklog(
100, // max concurrent handlers
50, // max queued requests
30*time.Second, // max time a request may wait in the queue
))
Parameters:
limit int— maximum number of handlers running simultaneously; must be > 0backlog int— maximum number of requests waiting for a slot; must be ≥ 0timeout time.Duration— how long a queued request waits before receiving a 503
Timeout
Cancels the request context after the specified duration. The handler is expected to honour ctx.Done() to exit early.
mux.Use(middleware.Timeout(10 * time.Second))
The timeout applies to the handler execution time, not to the total connection lifetime.
RequestID
Attaches a unique request ID to every request. Reads X-Request-Id from the incoming headers; generates a random UUID if absent. Writes the ID back in the response as X-Request-Id.
mux.Use(middleware.RequestID)
To read the request ID in a handler:
id := r.Header.Get("X-Request-Id")
RealIP
Extracts the real client IP address from X-Forwarded-For or X-Real-IP headers set by a reverse proxy, and sets r.RemoteAddr to that value.
mux.Use(middleware.RealIP)
Only use this middleware if the server is behind a trusted reverse proxy. Accepting these headers from arbitrary clients is a security risk.
CleanPath
Redirects URLs with redundant components to their canonical form:
//users→/users/a/../users→/users/a/./users→/a/users
mux.Pre(middleware.CleanPath) // run before routing to avoid a redirect
StripSlashes
Removes trailing slashes from the URL path before routing. Unlike RedirectTrailingSlash, this modifies the request in-place without issuing a redirect.
mux.Pre(middleware.StripSlashes)
NoCache
Sets headers that instruct browsers and intermediaries not to cache the response.
mux.Use(middleware.NoCache)
Headers set: Cache-Control: no-cache, no-store, no-transform, must-revalidate, private, max-age=0, Pragma: no-cache, Expires: 0.
SetHeader
Sets a fixed response header for every request:
mux.Use(middleware.SetHeader("X-Content-Type-Options", "nosniff"))
mux.Use(middleware.SetHeader("X-Frame-Options", "DENY"))
mux.Use(middleware.SetHeader("Strict-Transport-Security", "max-age=31536000"))
WithValue
Stores a value in the request context. Useful for injecting configuration or feature flags:
mux.Use(middleware.WithValue("appEnv", "production"))
// In a handler:
env := r.Context().Value("appEnv").(string)
See Also
- Groups — applying middleware to a subset of routes
- Error Handling — error-returning handlers
- Cookbook — middleware composition patterns
Upstream source
The middleware chain composition and the Use / Pre ordering described above are implemented in handler.go in the upstream repository.
Common questions
How do I add a middleware that runs on every request?
Call m.Use(middleware) on the top-level mux before registering any routes. The middleware wraps every handler the router dispatches, so request-id, recovery, and structured logging are the typical candidates for global Use.
How do I order multiple middlewares?
The order of Use calls is the order of execution: the first call wraps the outermost layer (the request enters it first and leaves it last). Reverse the registration order to reverse the chain — there is no priority field.
When should I prefer Pre over Use?
Pick Pre for middleware that must run before the router resolves the route — typically RealIP, CleanPath, or a TLS-redirect step. Pre runs against the raw URL before pattern matching; Use runs after the route is selected and the handler chain is composed.