Go middleware is a function that takes an http.Handler and returns an http.Handler. That simplicity is the whole design. No framework-specific interface, no decorator pattern, no magic — just function composition. Here are the three middleware pieces every Go backend needs and how to implement them cleanly.
func(http.Handler) http.Handler. Apply middleware in reverse order of execution — the first one you pass wraps the outermost layer.How middleware works in Go
// The middleware type.
type Middleware func(http.Handler) http.Handler
// A minimal example: add a response header.
func ServerHeader(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Server", "taqnihub/api")
next.ServeHTTP(w, r)
})
}The pattern is always the same: do something before the handler, call next.ServeHTTP, optionally do something after. Short-circuit by returning without calling next.
Auth middleware
Auth middleware validates the token, injects the user ID into the context, and rejects the request if the token is missing or invalid.
type ctxKey string
const ctxKeyUserID ctxKey = "user_id"
func RequireAuth(secret string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
raw := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if raw == "" {
writeError(w, http.StatusUnauthorized, "missing_token", "Authorization required.", nil)
return
}
claims, err := auth.ParseToken(raw, secret)
if err != nil {
writeError(w, http.StatusUnauthorized, "invalid_token", "Token is invalid or expired.", nil)
return
}
ctx := context.WithValue(r.Context(), ctxKeyUserID, claims.UserID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// Helper for handlers to read the user ID.
func UserIDFromCtx(ctx context.Context) (string, bool) {
id, ok := ctx.Value(ctxKeyUserID).(string)
return id, ok
}Structured logging middleware
A response writer wrapper captures the status code for logging — the standard http.ResponseWriterdoesn't expose it after the fact.
type statusWriter struct {
http.ResponseWriter
status int
}
func (sw *statusWriter) WriteHeader(code int) {
sw.status = code
sw.ResponseWriter.WriteHeader(code)
}
func Logger(logger *slog.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(sw, r)
logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", sw.status,
"dur_ms", time.Since(start).Milliseconds(),
)
})
}
}Rate limiting
A token-bucket limiter per IP using golang.org/x/time/rate. Store a limiter per IP in a sync.Map to avoid a global mutex on the hot path.
func RateLimiter(rps float64, burst int) Middleware {
var clients sync.Map
getLimiter := func(ip string) *rate.Limiter {
v, _ := clients.LoadOrStore(ip, rate.NewLimiter(rate.Limit(rps), burst))
return v.(*rate.Limiter)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
if !getLimiter(ip).Allow() {
writeError(w, http.StatusTooManyRequests, "rate_limit_exceeded", "Too many requests.", nil)
return
}
next.ServeHTTP(w, r)
})
}
}Middleware ordering
Middleware executes in the order it's applied. Apply outermost-first — recovery and logging wrap everything; auth applies only to protected routes.
r.Use(Recoverer) // 1st — catch panics before logging
r.Use(Logger(logger)) // 2nd — log every request
r.Use(RateLimiter(10, 50)) // 3rd — reject excess traffic
// RequireAuth applied per-route or per-group:
r.Group(func(r chi.Router) {
r.Use(RequireAuth(cfg.JWTSecret))
r.Get("/me", h.GetProfile)
})See the HTTP server post for the router setup that hosts these, and the JWT auth post for the token logic behind RequireAuth.
End of article · Thanks for reading