Frontend Labs
⋯

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/http middleware 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.


Generated from week4.md · 8 Oct 2026 16:36