HTTP middleware wraps a handler to run logic before or after the request. The standard shape is a function from http.Handler to http.Handler.
Write one middleware
type Middleware func(http.Handler) http.Handler
func requestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := cryptoRandID()
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), requestIDKey{}, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Middleware should pass the original request or a deliberate copy to next. If it decides the request is invalid, it writes a response and returns without calling the next handler.
Compose a chain
func chain(handler http.Handler, middleware ...Middleware) http.Handler {
for i := len(middleware) - 1; i >= 0; i-- {
handler = middleware[i](handler)
}
return handler
}
handler := chain(mux, recoverPanic, requestID, accessLog)
The first middleware listed is the outermost wrapper. Ordering matters: recovery should normally surround anything that might panic; request IDs should exist before logging needs them; authentication should run before protected handlers.
Recover without hiding failures
func recoverPanic(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if value := recover(); value != nil {
log.Printf("panic: %v\n%s", value, debug.Stack())
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
Recovery keeps one request from terminating the process, but it must record enough information to debug the defect.
Response-writer wrappers need care
Logging status codes requires a wrapper around http.ResponseWriter. Preserve optional interfaces such as http.Flusher when streaming or WebSockets matter. A simplistic wrapper can silently break handlers.
Keep middleware focused, dependency-injected, and independently testable with httptest. Business rules belong in services or handlers, not a growing global chain.