Day 24 — Functional Patterns & Middleware
Learning Objectives
- Build the middleware chain pattern used throughout your security proxy
- Use the options pattern for clean, extensible constructors
- Understand how
net/httpmiddleware composes
Key Concepts
// Middleware type — wraps a handler with additional behavior
type Middleware func(http.Handler) http.Handler
// Chain applies middleware in order: first middleware is outermost
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
// Chain(handler, A, B, C) → request flows: A → B → C → handler → C → B → A
// Options pattern — extensible constructors without config struct in signature
type Server struct { port int; timeout time.Duration; tlsCert string }
type Option func(*Server)
func WithPort(port int) Option { return func(s *Server) { s.port = port } }
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithTLS(cert string) Option { return func(s *Server) { s.tlsCert = cert } }
func NewServer(opts ...Option) *Server {
s := &Server{port: 8080, timeout: 30 * time.Second} // defaults
for _, opt := range opts {
opt(s)
}
return s
}
Exercise: Security Middleware Chain
package main
import (
"fmt"
"log/slog"
"net/http"
"time"
)
// Middleware chain helpers
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := generateRequestID()
w.Header().Set("X-Request-ID", id)
r = r.WithContext(context.WithValue(r.Context(), requestIDKey, id))
next.ServeHTTP(w, r)
})
}
func Logger(logger *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()
wrap := &statusWriter{ResponseWriter: w, code: 200}
next.ServeHTTP(wrap, r)
logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", wrap.code,
"latency_ms", time.Since(start).Milliseconds(),
"client_ip", r.RemoteAddr,
)
})
}
}
func PanicRecoverer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
fmt.Printf("PANIC: %v\n", rec)
}
}()
next.ServeHTTP(w, r)
})
}
type statusWriter struct {
http.ResponseWriter
code int
}
func (sw *statusWriter) WriteHeader(code int) {
sw.code = code
sw.ResponseWriter.WriteHeader(code)
}
func main() {
logger := slog.Default()
// The handler — application logic
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Context().Value(requestIDKey).(string)
fmt.Fprintf(w, `{"request_id":%q,"message":"hello"}`, id)
})
// Build the middleware chain
// Order: PanicRecoverer → RequestID → Logger → handler
// (outermost first — panic recovery must be outermost)
chain := Chain(handler,
PanicRecoverer,
RequestID,
Logger(logger),
)
srv := NewServer(
WithPort(8080),
WithTimeout(30*time.Second),
)
fmt.Printf("Server starting on port %d\n", srv.port)
http.ListenAndServe(fmt.Sprintf(":%d", srv.port), chain)
}
Real-World Context
Your complete security proxy has this middleware chain (Day 82): IPAllowlist → RateLimit → Auth → WAF → SchemaValidation → Handler. The order is a security decision: rate limiting before auth prevents auth bypass attempts from exhausting rate limit budgets; WAF after auth means you know the user identity in WAF logs.