Skip to content
All essays
ArchitectureMarch 27, 202619 min

API Gateway Patterns: Building Scalable Backend Architectures

Master API Gateway patterns and build robust, scalable backend architectures

Ü
Ümit Uz
Mobile & Full Stack Developer

API Gateways have become essential components in modern distributed systems. They act as the single entry point for all client requests, handling cross-cutting concerns like authentication, rate limiting, routing, and composition. This comprehensive guide explores API Gateway patterns and implementation strategies.

What is an API Gateway?

An API Gateway sits between clients and backend services, providing a unified API interface while abstracting backend complexity. It handles request routing, composition, and protocol translation, allowing microservices to remain loosely coupled.

Core Responsibilities

  1. 1Request Routing: Direct requests to appropriate backend services
  2. 2Authentication & Authorization: Validate and enrich requests with user context
  3. 3Rate Limiting: Prevent abuse and ensure fair usage
  4. 4Load Balancing: Distribute traffic across service instances
  5. 5Request/Response Transformation: Convert between protocols and formats
  6. 6Caching: Reduce backend load with intelligent caching
  7. 7Monitoring & Logging: Track all API traffic for observability

Gateway Patterns

Pattern 1: Basic Routing Gateway

The simplest pattern routes requests based on URL paths:

typescript
// gateway/routes.ts
import { Router } from 'express';

const router = Router();

router.use('/users', (req, res, next) => {
  proxyRequest(req, res, 'http://user-service:3001');
});

router.use('/orders', (req, res, next) => {
  proxyRequest(req, res, 'http://order-service:3002');
});

router.use('/products', (req, res, next) => {
  proxyRequest(req, res, 'http://product-service:3003');
});

function proxyRequest(req, res, targetUrl) {
  const options = {
    method: req.method,
    headers: req.headers,
    body: req.body
  };
  
  fetch(`${targetUrl}${req.path}`, options)
    .then(response => {
      res.status(response.status);
      response.body.pipe(res);
    })
    .catch(err => {
      res.status(502).json({ error: 'Bad Gateway' });
    });
}

export default router;

Pattern 2: Aggregation Gateway

Combine multiple backend calls into a single response:

typescript
// gateway/aggregator.ts
interface UserProfile {
  id: string;
  name: string;
}

interface UserOrders {
  userId: string;
  orders: Order[];
}

interface UserRecommendations {
  userId: string;
  products: Product[];
}

async function getUserDashboard(userId: string) {
  const [user, orders, recommendations] = await Promise.all([
    fetch(`http://user-service/${userId}`).then(r => r.json()),
    fetch(`http://order-service/user/${userId}`).then(r => r.json()),
    fetch(`http://recommendation-service/${userId}`).then(r => r.json())
  ]);

  return {
    user,
    orders,
    recommendations
  };
}

gateway.get('/dashboard/:userId', async (req, res) => {
  try {
    const dashboard = await getUserDashboard(req.params.userId);
    res.json(dashboard);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch dashboard' });
  }
});

Pattern 3: BFF (Backend for Frontend) Gateway

Create specialized gateways for different client types:

typescript
// gateway/mobile-bff.ts
// Optimized for mobile clients
class MobileBFF {
  async getProducts(req, res) {
    // Mobile needs fewer fields
    const products = await productService.getAll();
    const mobileProducts = products.map(p => ({
      id: p.id,
      name: p.name,
      price: p.price,
      thumbnail: p.images[0]
    }));
    
    res.json(mobileProducts);
  }
}

// gateway/web-bff.ts
// Optimized for web clients
class WebBFF {
  async getProducts(req, res) {
    // Web needs full details
    const products = await productService.getAll();
    res.json(products);
  }
}

Authentication Patterns

JWT Validation

typescript
// gateway/auth.ts
import jwt from 'jsonwebtoken';

interface JwtPayload {
  userId: string;
  roles: string[];
}

async function validateToken(token: string): Promise<JwtPayload | null> {
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET) as JwtPayload;
    return payload;
  } catch (error) {
    return null;
  }
}

export async function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  
  const payload = await validateToken(token);
  
  if (!payload) {
    return res.status(401).json({ error: 'Invalid token' });
  }
  
  req.user = payload;
  next();
}

OAuth 2.0 Integration

typescript
// gateway/oauth.ts
import axios from 'axios';

async function exchangeCodeForToken(code: string) {
  const response = await axios.post('https://oauth-provider.com/token', {
    grant_type: 'authorization_code',
    client_id: process.env.OAUTH_CLIENT_ID,
    client_secret: process.env.OAUTH_CLIENT_SECRET,
    code,
    redirect_uri: process.env.OAUTH_REDIRECT_URI
  });
  
  return response.data;
}

async function refreshAccessToken(refreshToken: string) {
  const response = await axios.post('https://oauth-provider.com/token', {
    grant_type: 'refresh_token',
    client_id: process.env.OAUTH_CLIENT_ID,
    client_secret: process.env.OAUTH_CLIENT_SECRET,
    refresh_token: refreshToken
  });
  
  return response.data;
}

Rate Limiting Strategies

Token Bucket Algorithm

typescript
// gateway/rate-limiter.ts
class TokenBucket {
  private tokens: number;
  private lastRefill: number;

  constructor(
    private capacity: number,
    private refillRate: number // tokens per second
  ) {
    this.tokens = capacity;
    this.lastRefill = Date.now();
  }

  consume(tokens = 1): boolean {
    this.refill();
    
    if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return true;
    }
    
    return false;
  }

  private refill() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    const tokensToAdd = elapsed * this.refillRate;
    
    this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
    this.lastRefill = now;
  }
}

class RateLimiter {
  private buckets = new Map<string, TokenBucket>();

  constructor(
    private capacity: number,
    private refillRate: number
  ) {}

  isAllowed(identifier: string): boolean {
    if (!this.buckets.has(identifier)) {
      this.buckets.set(
        identifier,
        new TokenBucket(this.capacity, this.refillRate)
      );
    }
    
    return this.buckets.get(identifier)!.consume();
  }
}

export const rateLimiter = new RateLimiter(100, 10); // 100 tokens, 10 per second

Sliding Window Log

typescript
class SlidingWindowLog {
  private logs = new Map<string, number[]>();

  isAllowed(identifier: string, limit: number, windowMs: number): boolean {
    const now = Date.now();
    const windowStart = now - windowMs;
    
    if (!this.logs.has(identifier)) {
      this.logs.set(identifier, []);
    }
    
    const userLogs = this.logs.get(identifier)!;
    
    // Remove old entries
    const validLogs = userLogs.filter(timestamp => timestamp > windowStart);
    this.logs.set(identifier, validLogs);
    
    if (validLogs.length < limit) {
      validLogs.push(now);
      return true;
    }
    
    return false;
  }
}

Service Discovery

Dynamic service discovery enables resilient routing:

typescript
// gateway/service-discovery.ts
interface ServiceInstance {
  host: string;
  port: number;
  lastHealthCheck: number;
}

class ServiceRegistry {
  private services = new Map<string, ServiceInstance[]>();

  register(serviceName: string, instance: ServiceInstance) {
    if (!this.services.has(serviceName)) {
      this.services.set(serviceName, []);
    }
    
    this.services.get(serviceName)!.push(instance);
  }

  discover(serviceName: string): ServiceInstance | null {
    const instances = this.services.get(serviceName);
    
    if (!instances || instances.length === 0) {
      return null;
    }
    
    // Round-robin selection
    const now = Date.now();
    const healthyInstances = instances.filter(
      i => now - i.lastHealthCheck < 30000 // Last check within 30s
    );
    
    if (healthyInstances.length === 0) {
      return null;
    }
    
    const index = Math.floor(Math.random() * healthyInstances.length);
    return healthyInstances[index];
  }
}

export const serviceRegistry = new ServiceRegistry();

Circuit Breaker Pattern

Prevent cascading failures with circuit breakers:

typescript
// gateway/circuit-breaker.ts
enum CircuitState {
  CLOSED,
  OPEN,
  HALF_OPEN
}

class CircuitBreaker {
  private state = CircuitState.CLOSED;
  private failureCount = 0;
  private lastFailureTime = 0;
  private successCount = 0;

  constructor(
    private threshold: number,
    private timeout: number,
    private halfOpenAttempts: number
  ) {}

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === CircuitState.OPEN) {
      if (Date.now() - this.lastFailureTime > this.timeout) {
        this.state = CircuitState.HALF_OPEN;
        this.successCount = 0;
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    if (this.state === CircuitState.HALF_OPEN) {
      this.successCount++;
      if (this.successCount >= this.halfOpenAttempts) {
        this.state = CircuitState.CLOSED;
        this.failureCount = 0;
      }
    } else {
      this.failureCount = 0;
    }
  }

  private onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    
    if (this.failureCount >= this.threshold) {
      this.state = CircuitState.OPEN;
    }
  }
}

Caching Strategies

Response Caching

typescript
// gateway/cache.ts
import NodeCache from 'node-cache';

const cache = new NodeCache({ stdTTL: 300 }); // 5 minute default

export function cacheMiddleware(ttl?: number) {
  return (req, res, next) => {
    const key = `${req.method}:${req.originalUrl}`;
    const cached = cache.get(key);
    
    if (cached) {
      return res.json(cached);
    }
    
    // Store original res.json
    const originalJson = res.json.bind(res);
    
    // Override res.json to cache responses
    res.json = function(data) {
      cache.set(key, data, ttl);
      return originalJson(data);
    };
    
    next();
  };
}

Request Transformation

Convert between different API versions:

typescript
// gateway/transformer.ts
interface V1User {
  id: string;
  first_name: string;
  last_name: string;
}

interface V2User {
  id: string;
  name: {
    first: string;
    last: string;
  };
}

function transformV1ToV2(v1User: V1User): V2User {
  return {
    id: v1User.id,
    name: {
      first: v1User.first_name,
      last: v1User.last_name
    }
  };
}

gateway.get('/v2/users/:id', async (req, res) => {
  const v1Response = await fetch(
    `http://user-service/v1/users/${req.params.id}`
  );
  const v1User = await v1Response.json();
  const v2User = transformV1ToV2(v1User);
  res.json(v2User);
});

Monitoring and Observability

Comprehensive monitoring is essential:

typescript
// gateway/monitoring.ts
import Prometheus from 'prom-client';

const httpRequestDuration = new Prometheus.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration',
  labelNames: ['method', 'route', 'status_code']
});

export function monitoringMiddleware(req, res, next) {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    httpRequestDuration.observe(
      {
        method: req.method,
        route: req.route?.path || req.path,
        status_code: res.statusCode
      },
      duration
    );
  });
  
  next();
}

Conclusion

API Gateways are critical components in modern microservices architectures. By implementing the patterns and strategies in this guide, you can build robust, scalable gateways that handle cross-cutting concerns effectively while keeping your services loosely coupled.

Remember to start simple and add complexity only when needed. Over-engineering your gateway can become a bottleneck, so always measure and iterate based on real requirements.

Enjoyed this essay? Share it.

Related essays

Next essay
AIOps: Artificial Intelligence for IT Operations