Concept · Web services
Middleware
Wrap a handler in another handler to add logging, auth or recovery to every request.
Middleware is code that runs around every request: before the handler, after it, or both. In Go it's simply a function that takes an http.Handler and returns a new one that wraps it:
func Logging(log *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // call the wrapped handler
log.Info("request", "method", r.Method, "path", r.URL.Path, "took", time.Since(start))
})
}
}This works because http.Handler is a one-method interface. A wrapped handler is still a handler, so wrappers stack:
handler := Recover(Logging(log)(RequestID(Auth(mux))))The onion
Requests travel inward through the layers and responses travel back out:
Recover → Logging → RequestID → Auth → handler
Recover ← Logging ← RequestID ← Auth ←That's why order matters:
- Recovery goes outermost so it catches panics from every layer inside it.
- Request ID and logging go before auth, so rejected requests are still logged with an ID.
- Auth goes before the handlers it protects. It can stop the chain by writing a
401and not callingnext. - Timeouts and body-size limits go near the outside, so they cover all the work.
What belongs in middleware
Cross-cutting concerns that are the same for every route belong here: panic recovery, request IDs, structured access logs, metrics and tracing (logs, metrics and traces), authentication, CORS headers (same-origin policy), compression, and per-client rate limits.
Business logic doesn't belong here. "Does this user own this order?" depends on the route and the data, so it belongs in the handler or service.
Passing data inward
Middleware passes data to inner handlers through the request context:
type ctxKey struct{}
ctx := context.WithValue(r.Context(), ctxKey{}, user)
next.ServeHTTP(w, r.WithContext(ctx))Use an unexported key type so no other package can collide with it. Provide a typed getter like UserFrom(ctx). Keep context values for request-scoped data, not for optional parameters.
To read the status code after next runs, wrap the ResponseWriter in a small struct that records WriteHeader. An API gateway is the same idea moved out of the process and in front of many services.