API Gateway Patterns

Why This Matters - The Digital Front Door Revolution

Think of a sprawling shopping mall with hundreds of stores, each with its own entrance, security guards, and information desk. Customers would be confused, frustrated, and likely leave. This is what happens when microservices expose their APIs directly without a unified entry point.

API gateways are the professional front desk of your digital infrastructure. They provide a single, elegant entry point that handles authentication, routing, rate limiting, and transformation - allowing your services to focus on what they do best: business logic.

Real-world impact: Netflix processes over 2 billion requests per day through their API gateway, handling authentication, routing to hundreds of microservices, and rate limiting for millions of users. Stripe's gateway processes payment requests from around the world, applying security rules and routing to appropriate services while maintaining sub-millisecond response times.

Without API gateways, these companies would need to replicate authentication, rate limiting, and routing logic across hundreds of services - a security nightmare and operational disaster.

Learning Objectives

By the end of this article, you will be able to:

  1. Design effective API gateway architectures for microservices environments
  2. Implement production-ready rate limiting algorithms
  3. Build robust authentication and authorization systems
  4. Create intelligent routing and request transformation mechanisms
  5. Apply resilience patterns for high availability
  6. Monitor and optimize gateway performance with comprehensive observability
  7. Scale gateway infrastructure to handle enterprise-level traffic

Core Concepts - Understanding Gateway Architecture

The Gateway Dilemma: Centralization vs Decentralization

Before diving into implementations, let's understand the fundamental trade-offs every API gateway must balance:

Centralization: Single point of control for consistency, security, and observability, but potential single point of failure
Decentralization: Individual service autonomy and resilience, but inconsistent security policies and operational complexity

πŸ’‘ Critical Insight: The right gateway strategy balances these concerns by providing centralized control while maintaining service autonomy through well-defined contracts and protocols.

Gateway Responsibilities: More Than Just Routing

A production API gateway handles multiple cross-cutting concerns:

 1/*
 2API Gateway Responsibilities Visualized:
 3
 4Client Request Gateway Journey
 5β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 6β”‚   Client    β”‚ β†’  β”‚                           API GATEWAY                                                β”‚
 7β”‚             β”‚    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
 8β”‚             β”‚    β”‚  β”‚   Auth &   β”‚   Rate       β”‚    Route    β”‚  Transform  β”‚   Monitor   β”‚    β”‚
 9β”‚             β”‚    β”‚  β”‚ Authorizationβ”‚   Limiting    β”‚              β”‚              β”‚   & Log     β”‚    β”‚
10β”‚             β”‚    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
11β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚                                                                                     β”‚
12                  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
13                  β†’  β”‚                    BACKEND SERVICES                                           β”‚
14                     β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
15                     β”‚  β”‚   User      β”‚    Order     β”‚   Payment   β”‚   Product   β”‚  Inventory  β”‚    β”‚
16                     β”‚  β”‚  Service    β”‚   Service    β”‚   Service    β”‚   Service    β”‚   Service   β”‚    β”‚
17                     β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
18                     β”‚                                                                                     β”‚
19                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
20
21Key Responsibilities:
221. Authentication & Authorization: Who is allowed to access what?
232. Rate Limiting: How many requests can clients make?
243. Request Routing: Which service should handle this request?
254. Request/Response Transformation: Convert formats, add headers, etc.
265. Circuit Breaking: Prevent cascade failures when services are down
276. Monitoring & Logging: Track all requests for debugging and analytics
28*/

Practical Examples - Building Production-Ready API Gateways

Gateway Foundation: Architecture and Core Components

Let's build a production-ready API gateway from scratch, focusing on reliability, performance, and maintainability.

Core Gateway Structure

  1package gateway
  2
  3import (
  4    "context"
  5    "fmt"
  6    "net/http"
  7    "sync"
  8    "time"
  9
 10    "github.com/go-chi/chi/v5"
 11    "github.com/go-chi/chi/v5/middleware"
 12)
 13
 14type APIGateway struct {
 15    // Core components
 16    router         *chi.Mux
 17    backends       map[string]*Backend
 18    middleware     []Middleware
 19    config         *Config
 20
 21    // Cross-cutting concerns
 22    authenticator Authenticator
 23    rateLimiter    RateLimiter
 24    circuitBreaker CircuitBreaker
 25    monitor        Monitor
 26
 27    // Runtime state
 28    stats          *GatewayStats
 29    healthChecker  *HealthChecker
 30}
 31
 32type Config struct {
 33    // Gateway settings
 34    Port            string           `yaml:"port"`
 35    ReadTimeout     time.Duration    `yaml:"read_timeout"`
 36    WriteTimeout    time.Duration    `yaml:"write_timeout"`
 37    ShutdownTimeout time.Duration    `yaml:"shutdown_timeout"`
 38
 39    // Security settings
 40    JWTSecret       string          `yaml:"jwt_secret"`
 41    RateLimitRPS    int             `yaml:"rate_limit_rps"`
 42    AllowedOrigins []string        `yaml:"allowed_origins"`
 43
 44    // Backend configuration
 45    Backends        []BackendConfig `yaml:"backends"`
 46
 47    // Circuit breaker settings
 48    CircuitBreakerTimeout time.Duration `yaml:"circuit_breaker_timeout"`
 49    CircuitBreakerThreshold int     `yaml:"circuit_breaker_threshold"`
 50
 51    // Monitoring
 52    EnableMetrics   bool   `yaml:"enable_metrics"`
 53    MetricsEndpoint  string  `yaml:"metrics_endpoint"`
 54}
 55
 56type Backend struct {
 57    Name        string         `json:"name"`
 58    URL         string         `json:"url"`
 59    Client      *http.Client  `json:"-"`
 60    HealthURL   string         `json:"health_url"`
 61    Weight      int            `json:"weight"`
 62    IsHealthy   bool           `json:"is_healthy"`
 63    LastChecked time.Time       `json:"last_checked"`
 64}
 65
 66type Middleware func(http.Handler) http.Handler
 67
 68func NewAPIGateway(config *Config) {
 69    // Validate configuration
 70    if err := validateConfig(config); err != nil {
 71        return nil, fmt.Errorf("invalid gateway config: %w", err)
 72    }
 73
 74    // Create router with middleware
 75    r := chi.NewRouter()
 76
 77    // Apply global middleware
 78    r.Use(middleware.RequestID)
 79    r.Use(middleware.RealIP)
 80    r.Use(middleware.Recoverer)
 81    r.Use(middleware.Timeout(config.ReadTimeout))
 82    r.Use(corsMiddleware(config.AllowedOrigins))
 83    r.Use(loggingMiddleware())
 84
 85    gateway := &APIGateway{
 86        router:   r,
 87        backends: make(map[string]*Backend),
 88        config:   config,
 89        stats:    NewGatewayStats(),
 90    }
 91
 92    // Initialize components
 93    if err := gateway.initializeComponents(); err != nil {
 94        return nil, fmt.Errorf("failed to initialize gateway components: %w", err)
 95    }
 96
 97    // Setup routes
 98    gateway.setupRoutes()
 99
100    // Start background processes
101    gateway.startBackgroundProcesses()
102
103    return gateway, nil
104}
105
106func initializeComponents() error {
107    // Initialize authenticator
108    auth, err := NewJWTAuthenticator(g.config.JWTSecret)
109    if err != nil {
110        return fmt.Errorf("failed to initialize authenticator: %w", err)
111    }
112    g.authenticator = auth
113
114    // Initialize rate limiter
115    g.rateLimiter = NewTokenBucketRateLimiter(g.config.RateLimitRPS)
116
117    // Initialize circuit breaker
118    g.circuitBreaker = NewCircuitBreaker(
119        g.config.CircuitBreakerThreshold,
120        g.config.CircuitBreakerTimeout,
121    )
122
123    // Initialize monitor
124    g.monitor = NewPrometheusMonitor()
125
126    // Setup backends
127    for _, backendConfig := range g.config.Backends {
128        backend := &Backend{
129            Name:  backendConfig.Name,
130            URL:   backendConfig.URL,
131            Weight: backendConfig.Weight,
132            Client: &http.Client{
133                Timeout: g.config.WriteTimeout,
134                Transport: &http.Transport{
135                    MaxIdleConns:        100,
136                    MaxIdleConnsPerHost: 10,
137                    IdleConnTimeout:     90 * time.Second,
138                    TLSHandshakeTimeout: 10 * time.Second,
139                },
140            },
141            HealthURL: backendConfig.HealthURL,
142            IsHealthy: false,
143        }
144        g.backends[backendConfig.Name] = backend
145    }
146
147    // Initialize health checker
148    g.healthChecker = NewHealthChecker(g.backends, 30*time.Second)
149
150    return nil
151}
152
153func setupRoutes() {
154    // API routes
155    g.router.Route("/api/v1", func(r chi.Router) {
156        // Apply authentication middleware
157        r.Use(g.authMiddleware)
158
159        // Apply rate limiting
160        r.Use(g.rateLimitMiddleware)
161
162        // Apply circuit breaker
163        r.Use(g.circuitBreakerMiddleware)
164
165        // Backend routes
166        for _, backend := range g.backends {
167            r.Mount("/"+backend.Name, g.proxyHandler(backend))
168        }
169    })
170
171    // Health check endpoint
172    g.router.Get("/health", g.healthHandler)
173
174    // Metrics endpoint
175    if g.config.EnableMetrics {
176        g.router.Get(g.config.MetricsEndpoint, g.metricsHandler)
177    }
178
179    // Admin endpoints
180    g.router.Route("/admin", func(r chi.Router) {
181        r.Use(g.adminAuthMiddleware)
182        r.Get("/backends", g.listBackendsHandler)
183        r.Post("/backends/{name}/toggle", g.toggleBackendHandler)
184        r.Get("/stats", g.statsHandler)
185    })
186}
187
188func Start(ctx context.Context) error {
189    // Start health checker
190    go g.healthChecker.Start(ctx)
191
192    // Start HTTP server
193    server := &http.Server{
194        Addr:         g.config.Port,
195        Handler:      g.router,
196        ReadTimeout:   g.config.ReadTimeout,
197        WriteTimeout:  g.config.WriteTimeout,
198        IdleTimeout:  60 * time.Second,
199    }
200
201    log.Printf("Starting API Gateway on %s", g.config.Port)
202
203    // Graceful shutdown
204    go func() {
205        <-ctx.Done()
206        log.Println("Shutting down API Gateway...")
207
208        shutdownCtx, cancel := context.WithTimeout(context.Background(), g.config.ShutdownTimeout)
209        defer cancel()
210
211        if err := server.Shutdown(shutdownCtx); err != nil {
212            log.Printf("Gateway shutdown error: %v", err)
213        }
214    }()
215
216    return server.ListenAndServe()
217}

Advanced Rate Limiting: Token Bucket Implementation

  1package ratelimit
  2
  3import (
  4    "context"
  5    "fmt"
  6    "net/http"
  7    "sync"
  8    "time"
  9
 10    "github.com/redis/go-redis/v9"
 11)
 12
 13// TokenBucketRateLimiter implements the token bucket algorithm
 14type TokenBucketRateLimiter struct {
 15    capacity     int64         // Maximum tokens in bucket
 16    refillRate   int64         // Tokens per second refill rate
 17    buckets      map[string]*TokenBucket
 18    mu           sync.RWMutex
 19    redis        *redis.Client   // For distributed rate limiting
 20    distributed  bool           // Whether to use Redis
 21}
 22
 23type TokenBucket struct {
 24    tokens     int64
 25    lastRefill time.Time
 26    mu         sync.Mutex
 27}
 28
 29func NewTokenBucketRateLimiter(requestsPerSecond int) *TokenBucketRateLimiter {
 30    return &TokenBucketRateLimiter{
 31        capacity:   int64(requestsPerSecond * 2), // Allow burst capacity
 32        refillRate: int64(requestsPerSecond),
 33        buckets:    make(map[string]*TokenBucket),
 34    }
 35}
 36
 37func NewDistributedTokenBucketRateLimiter(redis *redis.Client, requestsPerSecond int) *TokenBucketRateLimiter {
 38    return &TokenBucketRateLimiter{
 39        capacity:    int64(requestsPerSecond * 2),
 40        refillRate:  int64(requestsPerSecond),
 41        buckets:     make(map[string]*TokenBucket),
 42        redis:       redis,
 43        distributed: true,
 44    }
 45}
 46
 47func Allow(key string) bool {
 48    if tbl.distributed {
 49        return tbl.allowDistributed(key)
 50    }
 51    return tbl.allowLocal(key)
 52}
 53
 54func allowLocal(key string) bool {
 55    tbl.mu.RLock()
 56    bucket, exists := tbl.buckets[key]
 57    tbl.mu.RUnlock()
 58
 59    if !exists {
 60        tbl.mu.Lock()
 61        // Double-check after acquiring write lock
 62        if bucket, exists = tbl.buckets[key]; !exists {
 63            bucket = &TokenBucket{
 64                tokens:     tbl.capacity,
 65                lastRefill: time.Now(),
 66            }
 67            tbl.buckets[key] = bucket
 68        }
 69        tbl.mu.Unlock()
 70    }
 71
 72    bucket.mu.Lock()
 73    defer bucket.mu.Unlock()
 74
 75    // Refill tokens based on elapsed time
 76    now := time.Now()
 77    elapsed := now.Sub(bucket.lastRefill).Seconds()
 78    tokensToAdd := int64(elapsed * float64(tbl.refillRate))
 79
 80    if tokensToAdd > 0 {
 81        bucket.tokens += tokensToAdd
 82        if bucket.tokens > tbl.capacity {
 83            bucket.tokens = tbl.capacity
 84        }
 85        bucket.lastRefill = now
 86    }
 87
 88    // Check if tokens are available
 89    if bucket.tokens > 0 {
 90        bucket.tokens--
 91        return true
 92    }
 93
 94    return false
 95}
 96
 97func allowDistributed(key string) bool {
 98    script := redis.NewScript(`
 99        local key = KEYS[1]
100        local capacity = tonumber(ARGV[1])
101        local refill_rate = tonumber(ARGV[2])
102        local now = tonumber(ARGV[3])
103
104        local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
105        local tokens = tonumber(bucket[1]) or capacity
106        local last_refill = tonumber(bucket[2]) or now
107
108        local elapsed = now - last_refill
109        local tokens_to_add = math.floor(elapsed * refill_rate)
110
111        tokens = math.min(capacity, tokens + tokens_to_add)
112
113        if tokens >= 1 then
114            tokens = tokens - 1
115            redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
116            redis.call('EXPIRE', key, 60) -- Auto-expire after 60 seconds
117            return 1
118        else
119            redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
120            redis.call('EXPIRE', key, 60)
121            return 0
122        end
123    `)
124
125    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
126    defer cancel()
127
128    now := time.Now().Unix()
129    result, err := script.Run(ctx, tbl.redis,
130        []string{fmt.Sprintf("ratelimit:%s", key)},
131        tbl.capacity, tbl.refillRate, now).Result()
132
133    if err != nil {
134        // Fallback to local rate limiting on Redis failure
135        return tbl.allowLocal(key)
136    }
137
138    return result.(int64) == 1
139}
140
141// RateLimitMiddleware applies rate limiting based on client IP
142func Middleware(next http.Handler) http.Handler {
143    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
144        // Extract client IP
145        clientIP := getClientIP(r)
146
147        // Generate unique key for this client
148        key := fmt.Sprintf("ip:%s", clientIP)
149
150        // Check rate limit
151        if !tbl.Allow(key) {
152            w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", tbl.capacity))
153            w.Header().Set("X-RateLimit-Remaining", "0")
154            w.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d",
155                time.Now().Add(time.Second).Unix()))
156
157            w.WriteHeader(http.StatusTooManyRequests)
158            w.Write([]byte(`{"error": "Rate limit exceeded"}`))
159            return
160        }
161
162        next.ServeHTTP(w, r)
163    })
164}
165
166func getClientIP(r *http.Request) string {
167    // Check X-Forwarded-For header first
168    forwarded := r.Header.Get("X-Forwarded-For")
169    if forwarded != "" {
170        // Take the first IP in the list
171        ips := strings.Split(forwarded, ",")
172        return strings.TrimSpace(ips[0])
173    }
174
175    // Check X-Real-IP header
176    realIP := r.Header.Get("X-Real-IP")
177    if realIP != "" {
178        return realIP
179    }
180
181    // Fall back to RemoteAddr
182    return strings.Split(r.RemoteAddr, ":")[0]
183}

JWT Authentication: Secure and Efficient

  1package auth
  2
  3import (
  4    "context"
  5    "net/http"
  6    "strings"
  7    "time"
  8
  9    "github.com/golang-jwt/jwt/v5"
 10)
 11
 12type JWTAuthenticator struct {
 13    secretKey    []byte
 14    issuer      string
 15    tokenExpiry time.Duration
 16}
 17
 18type Claims struct {
 19    UserID   string   `json:"user_id"`
 20    Email    string   `json:"email"`
 21    Roles    []string `json:"roles"`
 22    jwt.RegisteredClaims
 23}
 24
 25type UserInfo struct {
 26    UserID   string
 27    Email    string
 28    Roles    []string
 29}
 30
 31func NewJWTAuthenticator(secretKey, issuer string) {
 32    if len(secretKey) < 32 {
 33        return nil, fmt.Errorf("JWT secret key must be at least 32 characters")
 34    }
 35
 36    return &JWTAuthenticator{
 37        secretKey:    []byte(secretKey),
 38        issuer:       issuer,
 39        tokenExpiry: 24 * time.Hour, // Default expiry
 40    }, nil
 41}
 42
 43func GenerateToken(userID, email string, roles []string) {
 44    now := time.Now()
 45
 46    claims := Claims{
 47        UserID: userID,
 48        Email:  email,
 49        Roles:  roles,
 50        RegisteredClaims: jwt.RegisteredClaims{
 51            ExpiresAt: jwt.NewNumericDate(now.Add(j.tokenExpiry)),
 52            IssuedAt:  jwt.NewNumericDate(now),
 53            NotBefore: jwt.NewNumericDate(now),
 54            Issuer:    j.issuer,
 55        },
 56    }
 57
 58    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
 59    return token.SignedString(j.secretKey)
 60}
 61
 62func ValidateToken(tokenString string) {
 63    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) {
 64        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
 65            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
 66        }
 67        return j.secretKey, nil
 68    })
 69
 70    if err != nil {
 71        return nil, fmt.Errorf("failed to parse JWT token: %w", err)
 72    }
 73
 74    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
 75        return claims, nil
 76    }
 77
 78    return nil, fmt.Errorf("invalid JWT token")
 79}
 80
 81func Middleware(next http.Handler) http.Handler {
 82    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 83        // Extract token from Authorization header
 84        authHeader := r.Header.Get("Authorization")
 85        if authHeader == "" {
 86            http.Error(w, "Missing Authorization header", http.StatusUnauthorized)
 87            return
 88        }
 89
 90        // Check Bearer token format
 91        tokenParts := strings.Split(authHeader, " ")
 92        if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
 93            http.Error(w, "Invalid Authorization header format", http.StatusUnauthorized)
 94            return
 95        }
 96
 97        tokenString := tokenParts[1]
 98
 99        // Validate token
100        claims, err := j.ValidateToken(tokenString)
101        if err != nil {
102            http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
103            return
104        }
105
106        // Add user info to context
107        userInfo := &UserInfo{
108            UserID: claims.UserID,
109            Email:  claims.Email,
110            Roles:  claims.Roles,
111        }
112
113        ctx := context.WithValue(r.Context(), "user", userInfo)
114        next.ServeHTTP(w, r.WithContext(ctx))
115    })
116}
117
118// RequireRole middleware for role-based authorization
119func RequireRole(requiredRole string) func(http.Handler) http.Handler {
120    return func(next http.Handler) http.Handler {
121        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
122            userInfo, ok := r.Context().Value("user").(*UserInfo)
123            if !ok {
124                http.Error(w, "User not authenticated", http.StatusUnauthorized)
125                return
126            }
127
128            // Check if user has required role
129            hasRole := false
130            for _, role := range userInfo.Roles {
131                if role == requiredRole {
132                    hasRole = true
133                    break
134                }
135            }
136
137            if !hasRole {
138                http.Error(w, "Insufficient permissions", http.StatusForbidden)
139                return
140            }
141
142            next.ServeHTTP(w, r)
143        })
144    }
145}
146
147// GetUserFromContext extracts user info from request context
148func GetUserFromContext(r *http.Request) {
149    user, ok := r.Context().Value("user").(*UserInfo)
150    return user, ok
151}

Circuit Breaker: Preventing Cascade Failures

  1package circuitbreaker
  2
  3import (
  4    "context"
  5    "fmt"
  6    "net/http"
  7    "sync"
  8    "time"
  9)
 10
 11type State int
 12
 13const (
 14    StateClosed State = iota
 15    StateOpen
 16    StateHalfOpen
 17)
 18
 19type CircuitBreaker struct {
 20    name           string
 21    maxFailures    int
 22    resetTimeout   time.Duration
 23    state          State
 24    failures       int
 25    lastFailure    time.Time
 26    mu             sync.RWMutex
 27    onRequestStart  func(name string)
 28    onRequestEnd   func(name string, success bool, duration time.Duration)
 29}
 30
 31func NewCircuitBreaker(maxFailures int, resetTimeout time.Duration) *CircuitBreaker {
 32    return &CircuitBreaker{
 33        maxFailures:  maxFailures,
 34        resetTimeout:  resetTimeout,
 35        state:        StateClosed,
 36    }
 37}
 38
 39func Execute(fn func() error) error {
 40    if !cb.canExecute() {
 41        return fmt.Errorf("circuit breaker '%s' is open", cb.name)
 42    }
 43
 44    start := time.Now()
 45    if cb.onRequestStart != nil {
 46        cb.onRequestStart(cb.name)
 47    }
 48
 49    defer func() {
 50        duration := time.Since(start)
 51        if cb.onRequestEnd != nil {
 52            cb.onRequestEnd(cb.name, true, duration)
 53        }
 54    }()
 55
 56    err := fn()
 57    cb.recordResult(err)
 58    return err
 59}
 60
 61func canExecute() bool {
 62    cb.mu.RLock()
 63    defer cb.mu.RUnlock()
 64
 65    switch cb.state {
 66    case StateClosed:
 67        return true
 68    case StateOpen:
 69        // Check if we should transition to half-open
 70        if time.Since(cb.lastFailure) > cb.resetTimeout {
 71            return true
 72        }
 73        return false
 74    case StateHalfOpen:
 75        return true
 76    }
 77
 78    return false
 79}
 80
 81func recordResult(err error) {
 82    cb.mu.Lock()
 83    defer cb.mu.Unlock()
 84
 85    if err != nil {
 86        cb.failures++
 87        cb.lastFailure = time.Now()
 88
 89        if cb.failures >= cb.maxFailures {
 90            cb.state = StateOpen
 91        }
 92    } else {
 93        // Success - reset failure count
 94        if cb.state == StateHalfOpen {
 95            cb.state = StateClosed
 96        }
 97        cb.failures = 0
 98    }
 99}
100
101func GetState() State {
102    cb.mu.RLock()
103    defer cb.mu.RUnlock()
104    return cb.state
105}
106
107func Middleware(next http.Handler) http.Handler {
108    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
109        err := cb.Execute(func() error {
110            // Execute the handler
111            w2 := &responseWriter{
112                ResponseWriter: w,
113                statusCode:    http.StatusOK,
114            }
115
116            next.ServeHTTP(w2, r)
117
118            // Consider 5xx as failures
119            if w2.statusCode >= 500 {
120                return fmt.Errorf("server error: %d", w2.statusCode)
121            }
122
123            return nil
124        })
125
126        if err != nil {
127            if cb.GetState() == StateOpen {
128                http.Error(w, "Service temporarily unavailable", http.StatusServiceUnavailable)
129            } else {
130                http.Error(w, err.Error(), http.StatusInternalServerError)
131            }
132        }
133    })
134}
135
136// Custom response writer to capture status code
137type responseWriter struct {
138    http.ResponseWriter
139    statusCode int
140}
141
142func WriteHeader(code int) {
143    rw.statusCode = code
144    rw.ResponseWriter.WriteHeader(code)
145}

Common Patterns and Pitfalls - Learning from Production Experience

Pattern 1: Load Balancing with Health Checks

 1type LoadBalancer struct {
 2    backends      []*Backend
 3    currentIndex  int
 4    strategy      string // "round_robin", "weighted_round_robin", "least_connections"
 5    mu            sync.RWMutex
 6}
 7
 8func NewLoadBalancer(backends []*Backend, strategy string) *LoadBalancer {
 9    return &LoadBalancer{
10        backends: backends,
11        strategy:  strategy,
12    }
13}
14
15func NextBackend() *Backend {
16    lb.mu.RLock()
17    defer lb.mu.RUnlock()
18
19    // Filter healthy backends
20    healthyBackends := make([]*Backend, 0)
21    for _, backend := range lb.backends {
22        if backend.IsHealthy {
23            healthyBackends = append(healthyBackends, backend)
24        }
25    }
26
27    if len(healthyBackends) == 0 {
28        return nil // No healthy backends available
29    }
30
31    switch lb.strategy {
32    case "round_robin":
33        return lb.roundRobin(healthyBackends)
34    case "weighted_round_robin":
35        return lb.weightedRoundRobin(healthyBackends)
36    case "least_connections":
37        return lb.leastConnections(healthyBackends)
38    default:
39        return lb.roundRobin(healthyBackends)
40    }
41}
42
43func roundRobin(backends []*Backend) *Backend {
44    backend := backends[lb.currentIndex%len(backends)]
45    lb.currentIndex++
46    return backend
47}
48
49func weightedRoundRobin(backends []*Backend) *Backend {
50    // Implement weighted round-robin logic
51    totalWeight := 0
52    for _, backend := range backends {
53        totalWeight += backend.Weight
54    }
55
56    if totalWeight == 0 {
57        return backends[0] // Fallback to first backend
58    }
59
60    // Simple weighted selection
61    target := lb.currentIndex % totalWeight
62    currentWeight := 0
63
64    for _, backend := range backends {
65        currentWeight += backend.Weight
66        if target < currentWeight {
67            lb.currentIndex++
68            return backend
69        }
70    }
71
72    lb.currentIndex++
73    return backends[0]
74}

Pattern 2: Request Transformation

 1package transform
 2
 3type RequestTransformer interface {
 4    Transform(*http.Request)
 5}
 6
 7type HeaderTransformer struct {
 8    AddHeaders  map[string]string
 9    RemoveHeaders []string
10}
11
12func Transform(r *http.Request) {
13    // Create a copy of the request to avoid mutating original
14    req := r.Clone(r.Context())
15
16    // Remove headers
17    for _, header := range ht.RemoveHeaders {
18        req.Header.Del(header)
19    }
20
21    // Add headers
22    for key, value := range ht.AddHeaders {
23        req.Header.Set(key, value)
24    }
25
26    return req, nil
27}
28
29type JSONTransformer struct {
30    AddFields    map[string]interface{}
31    RemoveFields []string
32}
33
34func Transform(r *http.Request) {
35    if r.Method != http.MethodPost && r.Method != http.MethodPut {
36        return r, nil // Only transform POST/PUT requests
37    }
38
39    // Read and parse JSON body
40    var data map[string]interface{}
41    if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
42        return r, err
43    }
44
45    // Remove fields
46    for _, field := range jt.RemoveFields {
47        delete(data, field)
48    }
49
50    // Add fields
51    for key, value := range jt.AddFields {
52        data[key] = value
53    }
54
55    // Encode modified JSON
56    modifiedBody, err := json.Marshal(data)
57    if err != nil {
58        return r, err
59    }
60
61    // Create new request with modified body
62    req := r.Clone(r.Context())
63    req.Body = io.NopCloser(strings.NewReader(string(modifiedBody)))
64    req.ContentLength = int64(len(modifiedBody))
65
66    return req, nil
67}

Common Pitfalls and Solutions

Pitfall 1: Not Handling Backend Failures Gracefully

 1// BAD: No error handling for backend failures
 2func proxyRequest(backend *Backend, w http.ResponseWriter, r *http.Request) {
 3    resp, err := backend.Client.Do(r) // Can fail!
 4    if err != nil {
 5        http.Error(w, "Backend error", http.StatusInternalServerError)
 6        return
 7    }
 8    // ... handle response
 9}
10
11// GOOD: Proper error handling with retries and fallbacks
12func proxyRequestWithFallback(w http.ResponseWriter, r *http.Request) error {
13    // Try multiple backends if one fails
14    for attempt := 0; attempt < 3; attempt++ {
15        backend := g.loadBalancer.NextBackend()
16        if backend == nil {
17            return fmt.Errorf("no healthy backends available")
18        }
19
20        resp, err := g.makeRequest(backend, r)
21        if err == nil {
22            return g.copyResponse(w, resp)
23        }
24
25        // Backend failed, mark as unhealthy
26        g.markBackendUnhealthy(backend)
27
28        if attempt < 2 {
29            // Exponential backoff before retry
30            time.Sleep(time.Duration(1<<uint(attempt)) * time.Second)
31            continue
32        }
33    }
34
35    // All backends failed
36    return fmt.Errorf("all backend attempts failed")
37}

Pitfall 2: Not Implementing Proper Timeout Handling

 1// GOOD: Implement context-aware timeout handling
 2func makeRequestWithContext(backend *Backend, r *http.Request) {
 3    // Create context with timeout
 4    ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
 5    defer cancel()
 6
 7    // Clone request with new context
 8    req := r.WithContext(ctx)
 9
10    return backend.Client.Do(req)
11}

Integration and Mastery - Building Complete Gateway Solutions

Comprehensive Monitoring

 1package monitoring
 2
 3import (
 4    "context"
 5    "net/http"
 6    "time"
 7
 8    "github.com/prometheus/client_golang/prometheus"
 9    "github.com/prometheus/client_golang/prometheus/promauto"
10)
11
12type GatewayMonitor struct {
13    // Prometheus metrics
14    requestDuration    *prometheus.HistogramVec
15    requestsTotal     *prometheus.CounterVec
16    activeConnections *prometheus.GaugeVec
17    backendHealth     *prometheus.GaugeVec
18}
19
20func NewGatewayMonitor() *GatewayMonitor {
21    return &GatewayMonitor{
22        requestDuration: promauto.NewHistogramVec(
23            prometheus.HistogramOpts{
24                Name: "gateway_request_duration_seconds",
25                Help: "Request duration in seconds",
26                Buckets: prometheus.DefBuckets,
27            },
28            []string{"method", "path", "backend", "status"},
29        ),
30        requestsTotal: promauto.NewCounterVec(
31            prometheus.CounterOpts{
32                Name: "gateway_requests_total",
33                Help: "Total number of requests",
34            },
35            []string{"method", "path", "backend", "status"},
36        ),
37        activeConnections: promauto.NewGaugeVec(
38            prometheus.GaugeOpts{
39                Name: "gateway_active_connections",
40                Help: "Number of active connections",
41            },
42            []string{"backend"},
43        ),
44        backendHealth: promauto.NewGaugeVec(
45            prometheus.GaugeOpts{
46                Name: "gateway_backend_health",
47                Help: "Backend health status",
48            },
49            []string{"backend"},
50        ),
51    }
52}
53
54func RecordRequest(method, path, backend, status string, duration time.Duration) {
55    gm.requestDuration.WithLabelValues(method, path, backend, status).Observe(duration.Seconds())
56    gm.requestsTotal.WithLabelValues(method, path, backend, status).Inc()
57}
58
59func RecordBackendHealth(backend string, healthy bool) {
60    value := float64(0)
61    if healthy {
62        value = 1
63    }
64    gm.backendHealth.WithLabelValues(backend).Set(value)
65}
66
67func Middleware(next http.Handler) http.Handler {
68    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
69        start := time.Now()
70
71        // Create response writer to capture status
72        rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
73
74        next.ServeHTTP(rw, r)
75
76        duration := time.Since(start)
77        status := fmt.Sprintf("%d", rw.statusCode)
78        path := r.URL.Path
79        method := r.Method
80
81        // Record metrics
82        gm.RecordRequest(method, path, "unknown", status, duration)
83    })
84}

Configuration Management

 1type ConfigurationManager struct {
 2    config     *Config
 3    reloadChan chan *Config
 4    mu         sync.RWMutex
 5}
 6
 7func NewConfigurationManager(configFile string) {
 8    config, err := loadConfig(configFile)
 9    if err != nil {
10        return nil, err
11    }
12
13    cm := &ConfigurationManager{
14        config:     config,
15        reloadChan: make(chan *Config, 1),
16    }
17
18    // Start configuration watcher
19    go cm.watchConfigFile(configFile)
20
21    return cm, nil
22}
23
24func GetConfig() *Config {
25    cm.mu.RLock()
26    defer cm.mu.RUnlock()
27    return cm.config
28}
29
30func Reload() error {
31    cm.mu.Lock()
32    defer cm.mu.Unlock()
33
34    newConfig, err := loadConfig(cm.configFile)
35    if err != nil {
36        return err
37    }
38
39    cm.config = newConfig
40    return nil
41}

Advanced Rate Limiting Strategies

Multi-Tier Rate Limiting

Production API gateways need sophisticated rate limiting that considers multiple factors: IP addresses, API keys, user tiers, and endpoints.

  1package ratelimit
  2
  3import (
  4    "context"
  5    "fmt"
  6    "time"
  7)
  8
  9// MultiTierRateLimiter implements tiered rate limiting
 10type MultiTierRateLimiter struct {
 11    limiters map[string]*TokenBucketRateLimiter
 12    tiers    map[string]RateLimitTier
 13}
 14
 15type RateLimitTier struct {
 16    Name           string
 17    RequestsPerSec int
 18    BurstCapacity  int
 19    QuotaPerDay    int64
 20}
 21
 22// Define standard tiers
 23var (
 24    FreeTier = RateLimitTier{
 25        Name:           "free",
 26        RequestsPerSec: 10,
 27        BurstCapacity:  20,
 28        QuotaPerDay:    10000,
 29    }
 30
 31    ProTier = RateLimitTier{
 32        Name:           "pro",
 33        RequestsPerSec: 100,
 34        BurstCapacity:  200,
 35        QuotaPerDay:    1000000,
 36    }
 37
 38    EnterpriseTier = RateLimitTier{
 39        Name:           "enterprise",
 40        RequestsPerSec: 1000,
 41        BurstCapacity:  2000,
 42        QuotaPerDay:    -1, // Unlimited
 43    }
 44)
 45
 46func NewMultiTierRateLimiter() *MultiTierRateLimiter {
 47    return &MultiTierRateLimiter{
 48        limiters: make(map[string]*TokenBucketRateLimiter),
 49        tiers: map[string]RateLimitTier{
 50            "free":       FreeTier,
 51            "pro":        ProTier,
 52            "enterprise": EnterpriseTier,
 53        },
 54    }
 55}
 56
 57// AllowRequest checks if request is allowed for given tier
 58func (m *MultiTierRateLimiter) AllowRequest(clientID, tier string) (bool, *RateLimitInfo) {
 59    tierConfig, exists := m.tiers[tier]
 60    if !exists {
 61        tierConfig = FreeTier // Default to free tier
 62    }
 63
 64    // Get or create limiter for this client
 65    key := fmt.Sprintf("%s:%s", tier, clientID)
 66    limiter, exists := m.limiters[key]
 67    if !exists {
 68        limiter = NewTokenBucketRateLimiter(tierConfig.RequestsPerSec)
 69        m.limiters[key] = limiter
 70    }
 71
 72    // Check rate limit
 73    allowed := limiter.Allow(clientID)
 74
 75    // Check daily quota if applicable
 76    if tierConfig.QuotaPerDay > 0 {
 77        dailyUsage := m.getDailyUsage(clientID)
 78        if dailyUsage >= tierConfig.QuotaPerDay {
 79            allowed = false
 80        }
 81    }
 82
 83    info := &RateLimitInfo{
 84        Tier:           tier,
 85        RequestsPerSec: tierConfig.RequestsPerSec,
 86        Remaining:      m.getRemainingQuota(clientID, tierConfig),
 87        ResetAt:        m.getResetTime(),
 88        Allowed:        allowed,
 89    }
 90
 91    if allowed {
 92        m.incrementUsage(clientID)
 93    }
 94
 95    return allowed, info
 96}
 97
 98type RateLimitInfo struct {
 99    Tier           string
100    RequestsPerSec int
101    Remaining      int64
102    ResetAt        time.Time
103    Allowed        bool
104}
105
106// Implement rate limit middleware with tier support
107func (m *MultiTierRateLimiter) Middleware(next http.Handler) http.Handler {
108    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
109        // Extract client ID and tier from context/headers
110        clientID := getClientID(r)
111        tier := getClientTier(r)
112
113        allowed, info := m.AllowRequest(clientID, tier)
114
115        // Set rate limit headers
116        w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", info.RequestsPerSec))
117        w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", info.Remaining))
118        w.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", info.ResetAt.Unix()))
119        w.Header().Set("X-RateLimit-Tier", info.Tier)
120
121        if !allowed {
122            w.WriteHeader(http.StatusTooManyRequests)
123            json.NewEncoder(w).Encode(map[string]interface{}{
124                "error":     "Rate limit exceeded",
125                "tier":      info.Tier,
126                "reset_at":  info.ResetAt,
127                "upgrade_url": "/pricing",
128            })
129            return
130        }
131
132        next.ServeHTTP(w, r)
133    })
134}
135
136func (m *MultiTierRateLimiter) getDailyUsage(clientID string) int64 {
137    // Implementation would query Redis or similar storage
138    return 0
139}
140
141func (m *MultiTierRateLimiter) getRemainingQuota(clientID string, tier RateLimitTier) int64 {
142    if tier.QuotaPerDay < 0 {
143        return -1 // Unlimited
144    }
145    return tier.QuotaPerDay - m.getDailyUsage(clientID)
146}
147
148func (m *MultiTierRateLimiter) getResetTime() time.Time {
149    now := time.Now()
150    tomorrow := now.Add(24 * time.Hour)
151    return time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, now.Location())
152}
153
154func (m *MultiTierRateLimiter) incrementUsage(clientID string) {
155    // Implementation would increment counter in Redis
156}

Adaptive Rate Limiting

Implement intelligent rate limiting that adapts to backend health and load:

  1// run
  2package main
  3
  4import (
  5    "context"
  6    "fmt"
  7    "sync"
  8    "sync/atomic"
  9    "time"
 10)
 11
 12// AdaptiveRateLimiter adjusts limits based on backend health
 13type AdaptiveRateLimiter struct {
 14    baseLimit       int64
 15    currentLimit    int64
 16    minLimit        int64
 17    maxLimit        int64
 18    healthChecker   HealthChecker
 19    adjustmentRate  float64
 20    mu              sync.RWMutex
 21}
 22
 23type HealthChecker interface {
 24    GetHealthScore() float64 // Returns 0.0 to 1.0
 25}
 26
 27func NewAdaptiveRateLimiter(baseLimit int64, health HealthChecker) *AdaptiveRateLimiter {
 28    return &AdaptiveRateLimiter{
 29        baseLimit:      baseLimit,
 30        currentLimit:   baseLimit,
 31        minLimit:       baseLimit / 10,
 32        maxLimit:       baseLimit * 2,
 33        healthChecker:  health,
 34        adjustmentRate: 0.1,
 35    }
 36}
 37
 38// Start begins monitoring and adjusting rate limits
 39func (a *AdaptiveRateLimiter) Start(ctx context.Context) {
 40    ticker := time.NewTicker(10 * time.Second)
 41    defer ticker.Stop()
 42
 43    for {
 44        select {
 45        case <-ctx.Done():
 46            return
 47        case <-ticker.C:
 48            a.adjustLimits()
 49        }
 50    }
 51}
 52
 53func (a *AdaptiveRateLimiter) adjustLimits() {
 54    healthScore := a.healthChecker.GetHealthScore()
 55
 56    a.mu.Lock()
 57    defer a.mu.Unlock()
 58
 59    // Calculate new limit based on health score
 60    targetLimit := int64(float64(a.baseLimit) * healthScore)
 61
 62    // Gradually adjust current limit
 63    if targetLimit > a.currentLimit {
 64        // Increase slowly
 65        a.currentLimit = int64(float64(a.currentLimit) * (1 + a.adjustmentRate))
 66    } else {
 67        // Decrease quickly
 68        a.currentLimit = int64(float64(a.currentLimit) * (1 - a.adjustmentRate*2))
 69    }
 70
 71    // Enforce bounds
 72    if a.currentLimit < a.minLimit {
 73        a.currentLimit = a.minLimit
 74    }
 75    if a.currentLimit > a.maxLimit {
 76        a.currentLimit = a.maxLimit
 77    }
 78
 79    fmt.Printf("Rate limit adjusted: %d (health: %.2f)\n", a.currentLimit, healthScore)
 80}
 81
 82func (a *AdaptiveRateLimiter) GetCurrentLimit() int64 {
 83    a.mu.RLock()
 84    defer a.mu.RUnlock()
 85    return a.currentLimit
 86}
 87
 88// Simple health checker implementation
 89type SimpleHealthChecker struct {
 90    errorRate   atomic.Value // float64
 91    latency     atomic.Value // time.Duration
 92}
 93
 94func (h *SimpleHealthChecker) RecordRequest(duration time.Duration, err error) {
 95    // Update latency
 96    h.latency.Store(duration)
 97
 98    // Update error rate
 99    currentRate, _ := h.errorRate.Load().(float64)
100    if err != nil {
101        h.errorRate.Store(currentRate*0.9 + 0.1) // Increase error rate
102    } else {
103        h.errorRate.Store(currentRate * 0.95) // Decrease error rate
104    }
105}
106
107func (h *SimpleHealthChecker) GetHealthScore() float64 {
108    errorRate, _ := h.errorRate.Load().(float64)
109    latency, _ := h.latency.Load().(time.Duration)
110
111    // Calculate score based on error rate and latency
112    errorScore := 1.0 - errorRate
113
114    latencyScore := 1.0
115    if latency > 0 {
116        targetLatency := 100 * time.Millisecond
117        if latency > targetLatency {
118            latencyScore = float64(targetLatency) / float64(latency)
119        }
120    }
121
122    return (errorScore * 0.7) + (latencyScore * 0.3)
123}
124
125func main() {
126    healthChecker := &SimpleHealthChecker{}
127    limiter := NewAdaptiveRateLimiter(1000, healthChecker)
128
129    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
130    defer cancel()
131
132    go limiter.Start(ctx)
133
134    // Simulate varying health conditions
135    for i := 0; i < 6; i++ {
136        // Simulate requests
137        if i%2 == 0 {
138            // Healthy requests
139            healthChecker.RecordRequest(50*time.Millisecond, nil)
140        } else {
141            // Unhealthy requests
142            healthChecker.RecordRequest(500*time.Millisecond, fmt.Errorf("backend error"))
143        }
144
145        time.Sleep(12 * time.Second)
146        fmt.Printf("Current limit: %d\n", limiter.GetCurrentLimit())
147    }
148}

Advanced Authentication and Authorization

API Key Management System

Production gateways need sophisticated API key management with scopes, rate limits, and rotation:

  1package auth
  2
  3import (
  4    "context"
  5    "crypto/rand"
  6    "crypto/sha256"
  7    "encoding/base64"
  8    "encoding/hex"
  9    "fmt"
 10    "time"
 11)
 12
 13// APIKeyManager manages API keys with scopes and permissions
 14type APIKeyManager struct {
 15    store APIKeyStore
 16}
 17
 18type APIKey struct {
 19    ID            string
 20    Key           string // Hashed key stored in database
 21    PlainKey      string // Only available during creation
 22    ClientID      string
 23    Name          string
 24    Scopes        []string
 25    RateLimit     int
 26    Tier          string
 27    CreatedAt     time.Time
 28    ExpiresAt     *time.Time
 29    LastUsedAt    *time.Time
 30    IsActive      bool
 31    Metadata      map[string]string
 32}
 33
 34type APIKeyStore interface {
 35    Create(ctx context.Context, key *APIKey) error
 36    GetByHash(ctx context.Context, hash string) (*APIKey, error)
 37    Update(ctx context.Context, key *APIKey) error
 38    Revoke(ctx context.Context, keyID string) error
 39    List(ctx context.Context, clientID string) ([]*APIKey, error)
 40}
 41
 42func NewAPIKeyManager(store APIKeyStore) *APIKeyManager {
 43    return &APIKeyManager{store: store}
 44}
 45
 46// GenerateAPIKey creates a new API key
 47func (m *APIKeyManager) GenerateAPIKey(ctx context.Context, req APIKeyRequest) (*APIKey, error) {
 48    // Generate secure random key
 49    plainKey, err := generateSecureKey()
 50    if err != nil {
 51        return nil, fmt.Errorf("failed to generate key: %w", err)
 52    }
 53
 54    // Hash the key for storage
 55    hashedKey := hashKey(plainKey)
 56
 57    key := &APIKey{
 58        ID:        generateID(),
 59        Key:       hashedKey,
 60        PlainKey:  plainKey, // Only returned once
 61        ClientID:  req.ClientID,
 62        Name:      req.Name,
 63        Scopes:    req.Scopes,
 64        RateLimit: req.RateLimit,
 65        Tier:      req.Tier,
 66        CreatedAt: time.Now(),
 67        IsActive:  true,
 68        Metadata:  req.Metadata,
 69    }
 70
 71    if req.ExpiresInDays > 0 {
 72        expiresAt := time.Now().Add(time.Duration(req.ExpiresInDays) * 24 * time.Hour)
 73        key.ExpiresAt = &expiresAt
 74    }
 75
 76    if err := m.store.Create(ctx, key); err != nil {
 77        return nil, fmt.Errorf("failed to store key: %w", err)
 78    }
 79
 80    return key, nil
 81}
 82
 83type APIKeyRequest struct {
 84    ClientID      string
 85    Name          string
 86    Scopes        []string
 87    RateLimit     int
 88    Tier          string
 89    ExpiresInDays int
 90    Metadata      map[string]string
 91}
 92
 93// ValidateAPIKey validates an API key and returns its metadata
 94func (m *APIKeyManager) ValidateAPIKey(ctx context.Context, plainKey string) (*APIKey, error) {
 95    hashedKey := hashKey(plainKey)
 96
 97    key, err := m.store.GetByHash(ctx, hashedKey)
 98    if err != nil {
 99        return nil, fmt.Errorf("invalid API key")
100    }
101
102    // Check if key is active
103    if !key.IsActive {
104        return nil, fmt.Errorf("API key has been revoked")
105    }
106
107    // Check expiration
108    if key.ExpiresAt != nil && time.Now().After(*key.ExpiresAt) {
109        return nil, fmt.Errorf("API key has expired")
110    }
111
112    // Update last used timestamp
113    now := time.Now()
114    key.LastUsedAt = &now
115    go m.store.Update(context.Background(), key)
116
117    return key, nil
118}
119
120// ValidateScope checks if the key has required scope
121func (m *APIKeyManager) ValidateScope(key *APIKey, requiredScope string) bool {
122    for _, scope := range key.Scopes {
123        if scope == requiredScope || scope == "*" {
124            return true
125        }
126    }
127    return false
128}
129
130// RevokeAPIKey revokes an API key
131func (m *APIKeyManager) RevokeAPIKey(ctx context.Context, keyID string) error {
132    return m.store.Revoke(ctx, keyID)
133}
134
135// RotateAPIKey creates a new key and revokes the old one
136func (m *APIKeyManager) RotateAPIKey(ctx context.Context, oldKeyID string) (*APIKey, error) {
137    // Get old key
138    oldKey, err := m.store.GetByHash(ctx, oldKeyID)
139    if err != nil {
140        return nil, err
141    }
142
143    // Generate new key with same properties
144    newKey, err := m.GenerateAPIKey(ctx, APIKeyRequest{
145        ClientID:  oldKey.ClientID,
146        Name:      oldKey.Name + " (rotated)",
147        Scopes:    oldKey.Scopes,
148        RateLimit: oldKey.RateLimit,
149        Tier:      oldKey.Tier,
150        Metadata:  oldKey.Metadata,
151    })
152    if err != nil {
153        return nil, err
154    }
155
156    // Revoke old key
157    if err := m.RevokeAPIKey(ctx, oldKeyID); err != nil {
158        return nil, err
159    }
160
161    return newKey, nil
162}
163
164// Helper functions
165func generateSecureKey() (string, error) {
166    b := make([]byte, 32)
167    if _, err := rand.Read(b); err != nil {
168        return "", err
169    }
170    return base64.URLEncoding.EncodeToString(b), nil
171}
172
173func hashKey(key string) string {
174    hash := sha256.Sum256([]byte(key))
175    return hex.EncodeToString(hash[:])
176}
177
178func generateID() string {
179    b := make([]byte, 16)
180    rand.Read(b)
181    return hex.EncodeToString(b)
182}
183
184// Middleware for API key authentication
185func (m *APIKeyManager) Middleware(next http.Handler) http.Handler {
186    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
187        // Extract API key from header
188        apiKey := r.Header.Get("X-API-Key")
189        if apiKey == "" {
190            http.Error(w, "Missing API key", http.StatusUnauthorized)
191            return
192        }
193
194        // Validate API key
195        key, err := m.ValidateAPIKey(r.Context(), apiKey)
196        if err != nil {
197            http.Error(w, err.Error(), http.StatusUnauthorized)
198            return
199        }
200
201        // Add key metadata to context
202        ctx := context.WithValue(r.Context(), "api_key", key)
203        next.ServeHTTP(w, r.WithContext(ctx))
204    })
205}
206
207// RequireScope middleware for scope-based authorization
208func (m *APIKeyManager) RequireScope(scope string) func(http.Handler) http.Handler {
209    return func(next http.Handler) http.Handler {
210        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
211            key, ok := r.Context().Value("api_key").(*APIKey)
212            if !ok {
213                http.Error(w, "API key not found", http.StatusUnauthorized)
214                return
215            }
216
217            if !m.ValidateScope(key, scope) {
218                http.Error(w, fmt.Sprintf("Missing required scope: %s", scope), http.StatusForbidden)
219                return
220            }
221
222            next.ServeHTTP(w, r)
223        })
224    }
225}

Intelligent Routing and Load Balancing

Content-Based Routing

Route requests based on content, headers, or request parameters:

  1package routing
  2
  3import (
  4    "net/http"
  5    "regexp"
  6    "strings"
  7)
  8
  9// ContentBasedRouter routes requests based on content
 10type ContentBasedRouter struct {
 11    rules []RoutingRule
 12}
 13
 14type RoutingRule struct {
 15    Name      string
 16    Matcher   RouteMatcher
 17    Backend   string
 18    Priority  int
 19    Rewrite   *RewriteRule
 20}
 21
 22type RouteMatcher interface {
 23    Match(r *http.Request) bool
 24}
 25
 26// Header matcher
 27type HeaderMatcher struct {
 28    Header string
 29    Value  *regexp.Regexp
 30}
 31
 32func (h *HeaderMatcher) Match(r *http.Request) bool {
 33    headerValue := r.Header.Get(h.Header)
 34    return h.Value.MatchString(headerValue)
 35}
 36
 37// Path matcher
 38type PathMatcher struct {
 39    Pattern *regexp.Regexp
 40}
 41
 42func (p *PathMatcher) Match(r *http.Request) bool {
 43    return p.Pattern.MatchString(r.URL.Path)
 44}
 45
 46// Query parameter matcher
 47type QueryMatcher struct {
 48    Param string
 49    Value *regexp.Regexp
 50}
 51
 52func (q *QueryMatcher) Match(r *http.Request) bool {
 53    queryValue := r.URL.Query().Get(q.Param)
 54    return q.Value.MatchString(queryValue)
 55}
 56
 57// Method matcher
 58type MethodMatcher struct {
 59    Methods []string
 60}
 61
 62func (m *MethodMatcher) Match(r *http.Request) bool {
 63    for _, method := range m.Methods {
 64        if r.Method == method {
 65            return true
 66        }
 67    }
 68    return false
 69}
 70
 71// Composite matcher
 72type CompositeMatcher struct {
 73    Matchers []RouteMatcher
 74    Operator string // "AND" or "OR"
 75}
 76
 77func (c *CompositeMatcher) Match(r *http.Request) bool {
 78    if c.Operator == "AND" {
 79        for _, matcher := range c.Matchers {
 80            if !matcher.Match(r) {
 81                return false
 82            }
 83        }
 84        return true
 85    } else { // OR
 86        for _, matcher := range c.Matchers {
 87            if matcher.Match(r) {
 88                return true
 89            }
 90        }
 91        return false
 92    }
 93}
 94
 95type RewriteRule struct {
 96    PathPattern     *regexp.Regexp
 97    PathReplacement string
 98    AddHeaders      map[string]string
 99    RemoveHeaders   []string
100}
101
102func NewContentBasedRouter() *ContentBasedRouter {
103    return &ContentBasedRouter{
104        rules: make([]RoutingRule, 0),
105    }
106}
107
108// AddRule adds a routing rule
109func (r *ContentBasedRouter) AddRule(rule RoutingRule) {
110    r.rules = append(r.rules, rule)
111
112    // Sort by priority
113    sort.Slice(r.rules, func(i, j int) bool {
114        return r.rules[i].Priority > r.rules[j].Priority
115    })
116}
117
118// SelectBackend selects the appropriate backend
119func (r *ContentBasedRouter) SelectBackend(req *http.Request) (string, *RewriteRule) {
120    for _, rule := range r.rules {
121        if rule.Matcher.Match(req) {
122            return rule.Backend, rule.Rewrite
123        }
124    }
125    return "", nil // Default backend
126}
127
128// Example usage
129func ExampleContentBasedRouting() {
130    router := NewContentBasedRouter()
131
132    // Route mobile traffic to mobile backend
133    router.AddRule(RoutingRule{
134        Name: "mobile-traffic",
135        Matcher: &HeaderMatcher{
136            Header: "User-Agent",
137            Value:  regexp.MustCompile(`(?i)mobile|android|iphone`),
138        },
139        Backend:  "mobile-backend",
140        Priority: 10,
141    })
142
143    // Route API v2 traffic
144    router.AddRule(RoutingRule{
145        Name: "api-v2",
146        Matcher: &PathMatcher{
147            Pattern: regexp.MustCompile(`^/api/v2/`),
148        },
149        Backend:  "api-v2-backend",
150        Priority: 9,
151        Rewrite: &RewriteRule{
152            PathPattern:     regexp.MustCompile(`^/api/v2/`),
153            PathReplacement: "/",
154        },
155    })
156
157    // Route beta users to beta backend
158    router.AddRule(RoutingRule{
159        Name: "beta-users",
160        Matcher: &CompositeMatcher{
161            Matchers: []RouteMatcher{
162                &HeaderMatcher{
163                    Header: "X-Beta-User",
164                    Value:  regexp.MustCompile(`true`),
165                },
166                &PathMatcher{
167                    Pattern: regexp.MustCompile(`^/api/`),
168                },
169            },
170            Operator: "AND",
171        },
172        Backend:  "beta-backend",
173        Priority: 8,
174    })
175}

Practice Exercises

Exercise 1: Implement Advanced Rate Limiting

Build a rate limiter that supports multiple algorithms and can switch between them.

Show Solution
 1package exercise
 2
 3type RateLimiter struct {
 4    algorithm string
 5    tokenBucket *TokenBucketRateLimiter
 6    slidingWindow *SlidingWindowRateLimiter
 7}
 8
 9func Allow(key string) bool {
10    switch rl.algorithm {
11    case "token_bucket":
12        return rl.tokenBucket.Allow(key)
13    case "sliding_window":
14        return rl.slidingWindow.Allow(key)
15    default:
16        return rl.tokenBucket.Allow(key) // Default to token bucket
17    }
18}

Exercise 2: Build Dynamic Configuration System

Implement a configuration system that can hot-reload without restarting the gateway.

Show Solution
 1package exercise
 2
 3type HotReloader struct {
 4    configPath string
 5    lastMod    time.Time
 6    config     *Config
 7    reloadChan chan *Config
 8}
 9
10func Watch() {
11    ticker := time.NewTicker(5 * time.Second)
12    for range ticker.C {
13        info, err := os.Stat(hr.configPath)
14        if err != nil {
15            continue
16        }
17
18        if info.ModTime().After(hr.lastMod) {
19            if newConfig, err := loadConfig(hr.configPath); err == nil {
20                hr.reloadChan <- newConfig
21                hr.lastMod = info.ModTime()
22            }
23        }
24    }
25}

Exercise 3: Implement Distributed Circuit Breaker

Build a circuit breaker that works across multiple gateway instances using Redis.

Show Solution
 1package exercise
 2
 3import (
 4    "context"
 5    "fmt"
 6    "time"
 7
 8    "github.com/redis/go-redis/v9"
 9)
10
11type DistributedCircuitBreaker struct {
12    name           string
13    redis          *redis.Client
14    timeout        time.Duration
15    maxFailures    int
16    resetTimeout   time.Duration
17}
18
19func NewDistributedCircuitBreaker(redis *redis.Client, name string, maxFailures int, resetTimeout time.Duration) *DistributedCircuitBreaker {
20    return &DistributedCircuitBreaker{
21        name:         name,
22        redis:        redis,
23        maxFailures:  maxFailures,
24        resetTimeout: resetTimeout,
25    }
26}
27
28func (dcb *DistributedCircuitBreaker) Execute(fn func() error) error {
29    ctx := context.Background()
30
31    // Check circuit state in Redis
32    state, err := dcb.getState(ctx)
33    if err == nil && state == "open" {
34        // Check if reset timeout has passed
35        lastFailure, _ := dcb.redis.Get(ctx, fmt.Sprintf("circuit:%s:last_failure", dcb.name)).Result()
36        if lastFailure != "" {
37            lastTime, _ := time.Parse(time.RFC3339, lastFailure)
38            if time.Since(lastTime) > dcb.resetTimeout {
39                // Try half-open state
40                dcb.setState(ctx, "half-open")
41            } else {
42                return fmt.Errorf("circuit breaker is open")
43            }
44        }
45    }
46
47    // Execute function
48    err = fn()
49
50    // Record result
51    if err != nil {
52        dcb.recordFailure(ctx)
53    } else {
54        dcb.recordSuccess(ctx)
55    }
56
57    return err
58}
59
60func (dcb *DistributedCircuitBreaker) getState(ctx context.Context) (string, error) {
61    return dcb.redis.Get(ctx, fmt.Sprintf("circuit:%s:state", dcb.name)).Result()
62}
63
64func (dcb *DistributedCircuitBreaker) setState(ctx context.Context, state string) error {
65    return dcb.redis.Set(ctx, fmt.Sprintf("circuit:%s:state", dcb.name), state, 24*time.Hour).Err()
66}
67
68func (dcb *DistributedCircuitBreaker) recordFailure(ctx context.Context) {
69    key := fmt.Sprintf("circuit:%s:failures", dcb.name)
70
71    // Increment failure count
72    failures, _ := dcb.redis.Incr(ctx, key).Result()
73    dcb.redis.Expire(ctx, key, dcb.resetTimeout)
74
75    // Record last failure time
76    dcb.redis.Set(ctx, fmt.Sprintf("circuit:%s:last_failure", dcb.name),
77        time.Now().Format(time.RFC3339), dcb.resetTimeout)
78
79    // Open circuit if threshold exceeded
80    if int(failures) >= dcb.maxFailures {
81        dcb.setState(ctx, "open")
82    }
83}
84
85func (dcb *DistributedCircuitBreaker) recordSuccess(ctx context.Context) {
86    state, _ := dcb.getState(ctx)
87
88    if state == "half-open" {
89        // Close circuit on success in half-open state
90        dcb.setState(ctx, "closed")
91        dcb.redis.Del(ctx, fmt.Sprintf("circuit:%s:failures", dcb.name))
92    }
93}

Exercise 4: Multi-Tier Rate Limiting with Redis

Learning Objective: Build a production-ready rate limiting system that supports multiple user tiers (free, pro, enterprise) with different rate limits and quotas.

Real-World Context: Companies like Stripe and Twilio use tiered rate limiting to provide different service levels to customers while protecting their infrastructure. This pattern is essential for any SaaS API platform.

Difficulty: Advanced | Time Estimate: 75-90 minutes

Objective: Implement a distributed rate limiter supporting multiple tiers with daily quotas, burst capacity, and Redis-based coordination.

Show Solution
  1package exercise
  2
  3import (
  4    "context"
  5    "fmt"
  6    "time"
  7
  8    "github.com/redis/go-redis/v9"
  9)
 10
 11// TieredRateLimiter implements multi-tier rate limiting
 12type TieredRateLimiter struct {
 13    redis *redis.Client
 14    tiers map[string]TierConfig
 15}
 16
 17type TierConfig struct {
 18    RequestsPerSecond int
 19    RequestsPerDay    int64
 20    BurstSize         int
 21}
 22
 23func NewTieredRateLimiter(redis *redis.Client) *TieredRateLimiter {
 24    return &TieredRateLimiter{
 25        redis: redis,
 26        tiers: map[string]TierConfig{
 27            "free": {
 28                RequestsPerSecond: 10,
 29                RequestsPerDay:    10000,
 30                BurstSize:         20,
 31            },
 32            "pro": {
 33                RequestsPerSecond: 100,
 34                RequestsPerDay:    1000000,
 35                BurstSize:         200,
 36            },
 37            "enterprise": {
 38                RequestsPerSecond: 1000,
 39                RequestsPerDay:    -1, // Unlimited
 40                BurstSize:         2000,
 41            },
 42        },
 43    }
 44}
 45
 46// AllowRequest checks if request is allowed for the given client and tier
 47func (rl *TieredRateLimiter) AllowRequest(ctx context.Context, clientID, tier string) (bool, *RateLimitStatus, error) {
 48    config, exists := rl.tiers[tier]
 49    if !exists {
 50        config = rl.tiers["free"] // Default to free tier
 51    }
 52
 53    // Check per-second rate limit using token bucket
 54    allowed, err := rl.checkTokenBucket(ctx, clientID, tier, config)
 55    if err != nil {
 56        return false, nil, err
 57    }
 58
 59    if !allowed {
 60        status := rl.getRateLimitStatus(ctx, clientID, tier, config)
 61        return false, status, nil
 62    }
 63
 64    // Check daily quota
 65    if config.RequestsPerDay > 0 {
 66        allowed, err = rl.checkDailyQuota(ctx, clientID, config)
 67        if err != nil {
 68            return false, nil, err
 69        }
 70
 71        if !allowed {
 72            status := rl.getRateLimitStatus(ctx, clientID, tier, config)
 73            return false, status, nil
 74        }
 75    }
 76
 77    // Increment counters
 78    rl.incrementCounters(ctx, clientID)
 79
 80    status := rl.getRateLimitStatus(ctx, clientID, tier, config)
 81    return true, status, nil
 82}
 83
 84type RateLimitStatus struct {
 85    Tier             string
 86    Limit            int
 87    Remaining        int64
 88    DailyLimit       int64
 89    DailyRemaining   int64
 90    ResetAt          time.Time
 91    DailyResetAt     time.Time
 92}
 93
 94// checkTokenBucket implements token bucket algorithm using Redis Lua script
 95func (rl *TieredRateLimiter) checkTokenBucket(ctx context.Context, clientID, tier string, config TierConfig) (bool, error) {
 96    script := redis.NewScript(`
 97        local key = KEYS[1]
 98        local capacity = tonumber(ARGV[1])
 99        local refill_rate = tonumber(ARGV[2])
100        local now = tonumber(ARGV[3])
101        local requested = 1
102
103        local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
104        local tokens = tonumber(bucket[1]) or capacity
105        local last_refill = tonumber(bucket[2]) or now
106
107        -- Calculate tokens to add based on time elapsed
108        local elapsed = now - last_refill
109        local tokens_to_add = math.floor(elapsed * refill_rate)
110
111        tokens = math.min(capacity, tokens + tokens_to_add)
112
113        if tokens >= requested then
114            tokens = tokens - requested
115            redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
116            redis.call('EXPIRE', key, 60)
117            return 1
118        else
119            redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
120            redis.call('EXPIRE', key, 60)
121            return 0
122        end
123    `)
124
125    key := fmt.Sprintf("ratelimit:token:%s:%s", tier, clientID)
126    now := time.Now().Unix()
127
128    result, err := script.Run(ctx, rl.redis,
129        []string{key},
130        config.BurstSize,
131        config.RequestsPerSecond,
132        now,
133    ).Result()
134
135    if err != nil {
136        return false, err
137    }
138
139    return result.(int64) == 1, nil
140}
141
142// checkDailyQuota checks if client hasn't exceeded daily quota
143func (rl *TieredRateLimiter) checkDailyQuota(ctx context.Context, clientID string, config TierConfig) (bool, error) {
144    key := fmt.Sprintf("ratelimit:daily:%s", clientID)
145
146    count, err := rl.redis.Get(ctx, key).Int64()
147    if err != nil && err != redis.Nil {
148        return false, err
149    }
150
151    return count < config.RequestsPerDay, nil
152}
153
154// incrementCounters increments usage counters
155func (rl *TieredRateLimiter) incrementCounters(ctx context.Context, clientID string) error {
156    dailyKey := fmt.Sprintf("ratelimit:daily:%s", clientID)
157
158    pipe := rl.redis.Pipeline()
159
160    // Increment daily counter
161    pipe.Incr(ctx, dailyKey)
162
163    // Set expiration to end of day if not set
164    pipe.ExpireAt(ctx, dailyKey, getEndOfDay())
165
166    _, err := pipe.Exec(ctx)
167    return err
168}
169
170// getRateLimitStatus returns current rate limit status
171func (rl *TieredRateLimiter) getRateLimitStatus(ctx context.Context, clientID, tier string, config TierConfig) *RateLimitStatus {
172    tokenKey := fmt.Sprintf("ratelimit:token:%s:%s", tier, clientID)
173    dailyKey := fmt.Sprintf("ratelimit:daily:%s", clientID)
174
175    // Get token bucket status
176    bucket, _ := rl.redis.HMGet(ctx, tokenKey, "tokens", "last_refill").Result()
177    tokens := int64(0)
178    if len(bucket) > 0 && bucket[0] != nil {
179        if val, ok := bucket[0].(string); ok {
180            fmt.Sscanf(val, "%d", &tokens)
181        }
182    }
183
184    // Get daily usage
185    dailyUsage, _ := rl.redis.Get(ctx, dailyKey).Int64()
186
187    status := &RateLimitStatus{
188        Tier:           tier,
189        Limit:          config.RequestsPerSecond,
190        Remaining:      tokens,
191        DailyLimit:     config.RequestsPerDay,
192        DailyRemaining: config.RequestsPerDay - dailyUsage,
193        ResetAt:        time.Now().Add(time.Second),
194        DailyResetAt:   getEndOfDay(),
195    }
196
197    if config.RequestsPerDay < 0 {
198        status.DailyRemaining = -1 // Unlimited
199    }
200
201    return status
202}
203
204func getEndOfDay() time.Time {
205    now := time.Now()
206    return time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
207}
208
209// Example HTTP middleware
210func (rl *TieredRateLimiter) Middleware(next http.Handler) http.Handler {
211    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
212        clientID := extractClientID(r)
213        tier := extractTier(r)
214
215        allowed, status, err := rl.AllowRequest(r.Context(), clientID, tier)
216        if err != nil {
217            http.Error(w, "Internal server error", http.StatusInternalServerError)
218            return
219        }
220
221        // Set rate limit headers
222        w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", status.Limit))
223        w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", status.Remaining))
224        w.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", status.ResetAt.Unix()))
225        w.Header().Set("X-RateLimit-Tier", status.Tier)
226
227        if status.DailyLimit > 0 {
228            w.Header().Set("X-RateLimit-Daily-Limit", fmt.Sprintf("%d", status.DailyLimit))
229            w.Header().Set("X-RateLimit-Daily-Remaining", fmt.Sprintf("%d", status.DailyRemaining))
230            w.Header().Set("X-RateLimit-Daily-Reset", fmt.Sprintf("%d", status.DailyResetAt.Unix()))
231        }
232
233        if !allowed {
234            w.WriteHeader(http.StatusTooManyRequests)
235            json.NewEncoder(w).Encode(map[string]interface{}{
236                "error":       "Rate limit exceeded",
237                "tier":        status.Tier,
238                "reset_at":    status.ResetAt,
239                "upgrade_url": "/pricing",
240            })
241            return
242        }
243
244        next.ServeHTTP(w, r)
245    })
246}
247
248func extractClientID(r *http.Request) string {
249    // Extract from API key, JWT, or IP
250    if apiKey := r.Header.Get("X-API-Key"); apiKey != "" {
251        return apiKey
252    }
253    return r.RemoteAddr
254}
255
256func extractTier(r *http.Request) string {
257    // Extract tier from context or header
258    if tier := r.Header.Get("X-Tier"); tier != "" {
259        return tier
260    }
261    return "free"
262}

Key Learning Points:

  1. Token Bucket Algorithm: Implements smooth rate limiting with burst capacity using Redis Lua scripts for atomic operations
  2. Multi-Tier Support: Different limits for free, pro, and enterprise tiers with daily quotas
  3. Distributed Coordination: Redis ensures rate limits work across multiple gateway instances
  4. Informative Headers: Provides clients with detailed rate limit information
  5. Graceful Degradation: Falls back to default tier on errors
  6. Production Patterns: Uses Redis pipelines, Lua scripts, and proper key expiration

Exercise 5: API Gateway with WebSocket Support and Load Balancing

Learning Objective: Build a comprehensive API gateway that handles both HTTP and WebSocket traffic with intelligent load balancing and session affinity.

Real-World Context: Modern applications like Slack and Discord use API gateways that must handle millions of persistent WebSocket connections alongside traditional HTTP requests, routing them efficiently to backend services.

Difficulty: Expert | Time Estimate: 90-110 minutes

Objective: Create a production-ready API gateway supporting WebSocket upgrades, sticky sessions, health checking, and graceful connection draining during deployments.

Show Solution
  1package exercise
  2
  3import (
  4    "context"
  5    "fmt"
  6    "net/http"
  7    "net/http/httputil"
  8    "net/url"
  9    "sync"
 10    "sync/atomic"
 11    "time"
 12
 13    "github.com/gorilla/websocket"
 14)
 15
 16// Gateway handles HTTP and WebSocket traffic
 17type Gateway struct {
 18    backends        []*Backend
 19    loadBalancer    LoadBalancer
 20    sessionStore    SessionStore
 21    healthChecker   *HealthChecker
 22    upgrader        websocket.Upgrader
 23    activeConns     int64
 24    mu              sync.RWMutex
 25}
 26
 27type Backend struct {
 28    URL           *url.URL
 29    Proxy         *httputil.ReverseProxy
 30    Weight        int
 31    IsHealthy     bool
 32    ActiveConns   int64
 33    TotalRequests int64
 34    mu            sync.RWMutex
 35}
 36
 37type LoadBalancer interface {
 38    SelectBackend(req *http.Request) *Backend
 39}
 40
 41type SessionStore interface {
 42    Get(sessionID string) (backendURL string, exists bool)
 43    Set(sessionID string, backendURL string, ttl time.Duration)
 44    Delete(sessionID string)
 45}
 46
 47func NewGateway(backendURLs []string) (*Gateway, error) {
 48    backends := make([]*Backend, 0, len(backendURLs))
 49
 50    for _, urlStr := range backendURLs {
 51        u, err := url.Parse(urlStr)
 52        if err != nil {
 53            return nil, fmt.Errorf("invalid backend URL %s: %w", urlStr, err)
 54        }
 55
 56        proxy := httputil.NewSingleHostReverseProxy(u)
 57        proxy.ErrorHandler = customErrorHandler
 58
 59        backend := &Backend{
 60            URL:       u,
 61            Proxy:     proxy,
 62            Weight:    1,
 63            IsHealthy: true,
 64        }
 65
 66        backends = append(backends, backend)
 67    }
 68
 69    g := &Gateway{
 70        backends:     backends,
 71        loadBalancer: NewWeightedRoundRobin(backends),
 72        sessionStore: NewMemorySessionStore(),
 73        upgrader: websocket.Upgrader{
 74            CheckOrigin: func(r *http.Request) bool {
 75                return true // Configure appropriately for production
 76            },
 77        },
 78    }
 79
 80    // Start health checker
 81    g.healthChecker = NewHealthChecker(backends, 10*time.Second)
 82    go g.healthChecker.Start(context.Background())
 83
 84    return g, nil
 85}
 86
 87// ServeHTTP handles incoming requests
 88func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 89    // Check if this is a WebSocket upgrade request
 90    if isWebSocketUpgrade(r) {
 91        g.handleWebSocket(w, r)
 92        return
 93    }
 94
 95    // Handle regular HTTP request
 96    g.handleHTTP(w, r)
 97}
 98
 99func (g *Gateway) handleHTTP(w http.ResponseWriter, r *http.Request) {
100    // Check for existing session
101    sessionID := extractSessionID(r)
102    var backend *Backend
103
104    if sessionID != "" {
105        // Try to get sticky session backend
106        if backendURL, exists := g.sessionStore.Get(sessionID); exists {
107            backend = g.findBackendByURL(backendURL)
108        }
109    }
110
111    // Select backend if no session or session backend not found
112    if backend == nil || !backend.IsHealthy {
113        backend = g.loadBalancer.SelectBackend(r)
114        if backend == nil {
115            http.Error(w, "No healthy backends available", http.StatusServiceUnavailable)
116            return
117        }
118
119        // Store session for sticky routing
120        if sessionID != "" {
121            g.sessionStore.Set(sessionID, backend.URL.String(), 1*time.Hour)
122        }
123    }
124
125    // Track connection
126    atomic.AddInt64(&backend.ActiveConns, 1)
127    atomic.AddInt64(&backend.TotalRequests, 1)
128    defer atomic.AddInt64(&backend.ActiveConns, -1)
129
130    // Proxy request
131    backend.Proxy.ServeHTTP(w, r)
132}
133
134func (g *Gateway) handleWebSocket(w http.ResponseWriter, r *http.Request) {
135    // Select backend (with session affinity if available)
136    sessionID := extractSessionID(r)
137    var backend *Backend
138
139    if sessionID != "" {
140        if backendURL, exists := g.sessionStore.Get(sessionID); exists {
141            backend = g.findBackendByURL(backendURL)
142        }
143    }
144
145    if backend == nil || !backend.IsHealthy {
146        backend = g.loadBalancer.SelectBackend(r)
147        if backend == nil {
148            http.Error(w, "No healthy backends available", http.StatusServiceUnavailable)
149            return
150        }
151    }
152
153    // Store session for this WebSocket connection
154    if sessionID == "" {
155        sessionID = generateSessionID()
156    }
157    g.sessionStore.Set(sessionID, backend.URL.String(), 24*time.Hour)
158
159    // Upgrade connection to WebSocket
160    clientConn, err := g.upgrader.Upgrade(w, r, nil)
161    if err != nil {
162        http.Error(w, "Failed to upgrade connection", http.StatusBadRequest)
163        return
164    }
165    defer clientConn.Close()
166
167    // Connect to backend WebSocket
168    backendWSURL := convertToWebSocketURL(backend.URL, r.URL.Path)
169    backendConn, _, err := websocket.DefaultDialer.Dial(backendWSURL, nil)
170    if err != nil {
171        clientConn.WriteMessage(websocket.CloseMessage,
172            websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "Backend unavailable"))
173        return
174    }
175    defer backendConn.Close()
176
177    // Track active connection
178    atomic.AddInt64(&g.activeConns, 1)
179    atomic.AddInt64(&backend.ActiveConns, 1)
180    defer func() {
181        atomic.AddInt64(&g.activeConns, -1)
182        atomic.AddInt64(&backend.ActiveConns, -1)
183    }()
184
185    // Bidirectional message forwarding
186    g.proxyWebSocket(clientConn, backendConn)
187}
188
189// proxyWebSocket forwards messages between client and backend
190func (g *Gateway) proxyWebSocket(client, backend *websocket.Conn) {
191    errChan := make(chan error, 2)
192
193    // Client -> Backend
194    go func() {
195        for {
196            messageType, data, err := client.ReadMessage()
197            if err != nil {
198                errChan <- err
199                return
200            }
201
202            if err := backend.WriteMessage(messageType, data); err != nil {
203                errChan <- err
204                return
205            }
206        }
207    }()
208
209    // Backend -> Client
210    go func() {
211        for {
212            messageType, data, err := backend.ReadMessage()
213            if err != nil {
214                errChan <- err
215                return
216            }
217
218            if err := client.WriteMessage(messageType, data); err != nil {
219                errChan <- err
220                return
221            }
222        }
223    }()
224
225    // Wait for error from either direction
226    <-errChan
227}
228
229// Weighted round-robin load balancer
230type WeightedRoundRobin struct {
231    backends     []*Backend
232    currentIndex int64
233    mu           sync.Mutex
234}
235
236func NewWeightedRoundRobin(backends []*Backend) *WeightedRoundRobin {
237    return &WeightedRoundRobin{
238        backends: backends,
239    }
240}
241
242func (wrr *WeightedRoundRobin) SelectBackend(req *http.Request) *Backend {
243    wrr.mu.Lock()
244    defer wrr.mu.Unlock()
245
246    // Filter healthy backends
247    healthyBackends := make([]*Backend, 0)
248    for _, backend := range wrr.backends {
249        if backend.IsHealthy {
250            healthyBackends = append(healthyBackends, backend)
251        }
252    }
253
254    if len(healthyBackends) == 0 {
255        return nil
256    }
257
258    // Simple round-robin (can be enhanced with actual weighting)
259    backend := healthyBackends[wrr.currentIndex%int64(len(healthyBackends))]
260    wrr.currentIndex++
261
262    return backend
263}
264
265// Health checker
266type HealthChecker struct {
267    backends []*Backend
268    interval time.Duration
269}
270
271func NewHealthChecker(backends []*Backend, interval time.Duration) *HealthChecker {
272    return &HealthChecker{
273        backends: backends,
274        interval: interval,
275    }
276}
277
278func (hc *HealthChecker) Start(ctx context.Context) {
279    ticker := time.NewTicker(hc.interval)
280    defer ticker.Stop()
281
282    for {
283        select {
284        case <-ctx.Done():
285            return
286        case <-ticker.C:
287            hc.checkHealth()
288        }
289    }
290}
291
292func (hc *HealthChecker) checkHealth() {
293    for _, backend := range hc.backends {
294        go func(b *Backend) {
295            healthURL := b.URL.String() + "/health"
296            resp, err := http.Get(healthURL)
297
298            b.mu.Lock()
299            defer b.mu.Unlock()
300
301            if err != nil || resp.StatusCode != http.StatusOK {
302                b.IsHealthy = false
303            } else {
304                b.IsHealthy = true
305            }
306
307            if resp != nil {
308                resp.Body.Close()
309            }
310        }(backend)
311    }
312}
313
314// Helper functions
315func isWebSocketUpgrade(r *http.Request) bool {
316    return r.Header.Get("Upgrade") == "websocket"
317}
318
319func extractSessionID(r *http.Request) string {
320    cookie, err := r.Cookie("session_id")
321    if err != nil {
322        return ""
323    }
324    return cookie.Value
325}
326
327func (g *Gateway) findBackendByURL(urlStr string) *Backend {
328    g.mu.RLock()
329    defer g.mu.RUnlock()
330
331    for _, backend := range g.backends {
332        if backend.URL.String() == urlStr {
333            return backend
334        }
335    }
336    return nil
337}
338
339func convertToWebSocketURL(baseURL *url.URL, path string) string {
340    scheme := "ws"
341    if baseURL.Scheme == "https" {
342        scheme = "wss"
343    }
344    return fmt.Sprintf("%s://%s%s", scheme, baseURL.Host, path)
345}
346
347func generateSessionID() string {
348    return fmt.Sprintf("%d", time.Now().UnixNano())
349}
350
351func customErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
352    http.Error(w, "Gateway error: "+err.Error(), http.StatusBadGateway)
353}
354
355// Simple memory-based session store
356type MemorySessionStore struct {
357    sessions map[string]sessionEntry
358    mu       sync.RWMutex
359}
360
361type sessionEntry struct {
362    backendURL string
363    expiresAt  time.Time
364}
365
366func NewMemorySessionStore() *MemorySessionStore {
367    store := &MemorySessionStore{
368        sessions: make(map[string]sessionEntry),
369    }
370
371    // Start cleanup goroutine
372    go store.cleanup()
373
374    return store
375}
376
377func (m *MemorySessionStore) Get(sessionID string) (string, bool) {
378    m.mu.RLock()
379    defer m.mu.RUnlock()
380
381    entry, exists := m.sessions[sessionID]
382    if !exists || time.Now().After(entry.expiresAt) {
383        return "", false
384    }
385
386    return entry.backendURL, true
387}
388
389func (m *MemorySessionStore) Set(sessionID string, backendURL string, ttl time.Duration) {
390    m.mu.Lock()
391    defer m.mu.Unlock()
392
393    m.sessions[sessionID] = sessionEntry{
394        backendURL: backendURL,
395        expiresAt:  time.Now().Add(ttl),
396    }
397}
398
399func (m *MemorySessionStore) Delete(sessionID string) {
400    m.mu.Lock()
401    defer m.mu.Unlock()
402
403    delete(m.sessions, sessionID)
404}
405
406func (m *MemorySessionStore) cleanup() {
407    ticker := time.NewTicker(1 * time.Minute)
408    defer ticker.Stop()
409
410    for range ticker.C {
411        m.mu.Lock()
412        now := time.Now()
413        for id, entry := range m.sessions {
414            if now.After(entry.expiresAt) {
415                delete(m.sessions, id)
416            }
417        }
418        m.mu.Unlock()
419    }
420}

Key Learning Points:

  1. WebSocket Support: Handles WebSocket upgrade requests and maintains persistent connections
  2. Bidirectional Proxying: Forwards messages in both directions between client and backend
  3. Sticky Sessions: Maintains session affinity for WebSocket connections
  4. Health Checking: Monitors backend health and routes away from unhealthy servers
  5. Load Balancing: Weighted round-robin with automatic failover
  6. Graceful Handling: Proper connection lifecycle management and cleanup
  7. Production Patterns: Atomic counters, proper locking, and error handling

Key Takeaways

  1. Security First: Always implement authentication and authorization before routing requests
  2. Rate Limiting: Protect your services from abuse and ensure fair usage
  3. Health Monitoring: Continuously monitor backend health and route away from failed services
  4. Circuit Breaking: Prevent cascade failures when services become unavailable
  5. Observability: Comprehensive logging and metrics for troubleshooting and optimization
  6. Configuration Management: Support hot reloading and environment-specific configurations
  7. Performance: Use connection pooling, request caching, and proper timeout handling

Production Checklist

  • Authentication: Implement JWT, API keys, and OAuth2 support
  • Authorization: Add role-based access control
  • Rate Limiting: Multiple algorithms with per-client limits
  • Load Balancing: Multiple strategies with health checks
  • Circuit Breaking: Prevent cascade failures
  • Monitoring: Comprehensive metrics and alerting
  • Security: HTTPS, CORS headers, input validation
  • Testing: Load testing, failure scenarios, and security testing

When You Need an API Gateway

  • Multiple microservices that need unified access control
  • Different client types with varying requirements
  • Cross-cutting concerns that shouldn't be duplicated in each service
  • Performance optimization through request aggregation and caching
  • Security requirements that need centralized enforcement
  • Compliance needs for auditing and access control

Final Insight: API gateways are the backbone of microservices architecture. Invest time in building them correctly with proper security, monitoring, and resilience patterns. A well-designed gateway enables your services to focus on business logic while maintaining security, performance, and reliability at scale.

Further Reading