TechLead
Lesson 9 of 30
7 min read
System Design

API Gateway Design

Learn API gateway design patterns including routing, authentication, rate limiting, request transformation, circuit breaker, and comparisons of Kong, AWS, and Nginx

What Is an API Gateway?

An API gateway is a server that sits between clients and backend services, acting as the single entry point for all API requests. Instead of clients communicating directly with multiple microservices, they send all requests to the gateway, which routes them to the appropriate service.

Think of it as the front door to your microservices architecture. It handles cross-cutting concerns — authentication, rate limiting, logging, request transformation — so that individual services do not have to implement these features themselves.

Why API Gateways Exist

Without an API gateway, a mobile app calling 10 microservices would need to:

  • Know every service URL: Client must manage connections to multiple services
  • Handle auth separately: Each service must validate tokens independently
  • Make multiple round trips: A single screen might require calls to 5-10 services
  • Handle protocol differences: Some services use REST, others gRPC, some WebSocket
  • Implement retries and timeouts: Each client must handle failures for each service

An API gateway centralizes all of this, giving clients a single endpoint and a consistent API.

Core Responsibilities

1. Request Routing

The gateway routes requests to the appropriate backend service based on the URL path, HTTP method, headers, or other request attributes. This decouples clients from the internal service topology — services can be split, merged, or moved without clients knowing.

// API Gateway routing configuration (conceptual)
interface RouteConfig {
  path: string;
  method: string;
  upstream: string;
  middleware: string[];
}

const routes: RouteConfig[] = [
  {
    path: '/api/users/*',
    method: 'ALL',
    upstream: 'http://user-service:3001',
    middleware: ['auth', 'rateLimit', 'logging'],
  },
  {
    path: '/api/orders/*',
    method: 'ALL',
    upstream: 'http://order-service:3002',
    middleware: ['auth', 'rateLimit', 'logging'],
  },
  {
    path: '/api/products/*',
    method: 'GET',
    upstream: 'http://product-service:3003',
    middleware: ['cache', 'rateLimit', 'logging'], // No auth for public product listing
  },
  {
    path: '/api/admin/*',
    method: 'ALL',
    upstream: 'http://admin-service:3004',
    middleware: ['auth', 'adminOnly', 'auditLog', 'rateLimit'],
  },
];

2. Authentication and Authorization

The gateway validates authentication tokens (JWT, API keys, OAuth tokens) before forwarding requests to backend services. This means backend services can trust that all incoming requests have been authenticated, simplifying their logic.

// Authentication middleware in API gateway
import jwt from 'jsonwebtoken';

async function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!);

    // Attach user info to headers for downstream services
    req.headers['x-user-id'] = decoded.userId;
    req.headers['x-user-role'] = decoded.role;
    req.headers['x-user-email'] = decoded.email;

    // Remove the original auth header - downstream services
    // trust the x-user-* headers set by the gateway
    delete req.headers.authorization;

    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

3. Rate Limiting

The gateway enforces rate limits to protect backend services from being overwhelmed. Rate limits can be applied per user, per API key, per IP address, or per endpoint.

4. Request/Response Transformation

The gateway can modify requests before forwarding them and responses before returning them. This is useful for API versioning, protocol translation, and response aggregation.

// Response aggregation - combine multiple service responses
async function getUserDashboard(req: Request, res: Response) {
  const userId = req.headers['x-user-id'];

  // Call multiple services in parallel
  const [profile, orders, notifications, recommendations] = await Promise.all([
    fetch(`http://user-service:3001/users/${userId}`).then(r => r.json()),
    fetch(`http://order-service:3002/users/${userId}/recent`).then(r => r.json()),
    fetch(`http://notification-service:3003/users/${userId}/unread`).then(r => r.json()),
    fetch(`http://recommendation-service:3004/users/${userId}`).then(r => r.json()),
  ]);

  // Aggregate into a single response
  // Client makes ONE call instead of FOUR
  res.json({
    profile,
    recentOrders: orders.slice(0, 5),
    unreadNotifications: notifications.count,
    recommendations: recommendations.items.slice(0, 10),
  });
}

5. Logging and Monitoring

The gateway is the ideal place to log all API traffic because every request passes through it. You can capture request/response metadata, latency, error rates, and usage patterns without instrumenting each individual service.

API Gateway Patterns

Backend for Frontend (BFF)

Instead of a single gateway for all clients, create a separate gateway for each client type (web, mobile, third-party). Each BFF is optimized for its client's needs — the mobile BFF might aggregate more data per request (to reduce round trips on slow networks), while the web BFF might return richer responses.

// Backend for Frontend pattern
//
//  Mobile App -----> [ Mobile BFF Gateway ] -----> [ User Service ]
//                                           -----> [ Order Service ]
//
//  Web App --------> [ Web BFF Gateway ]    -----> [ User Service ]
//                                           -----> [ Product Service ]
//
//  Partner API ----> [ Partner API Gateway ] -----> [ Order Service ]
//                                           -----> [ Inventory Service ]

Gateway Offloading

Move shared functionality from individual services to the gateway: SSL termination, compression, CORS handling, request validation. This reduces duplication and ensures consistency across services.

Circuit Breaker Pattern

The circuit breaker prevents the gateway from forwarding requests to a failing service, which would waste resources and increase latency. Like an electrical circuit breaker, it "trips" when too many requests fail, and stops sending traffic until the service recovers.

// Circuit Breaker implementation
enum CircuitState {
  CLOSED = 'CLOSED',       // Normal operation
  OPEN = 'OPEN',           // Rejecting all requests
  HALF_OPEN = 'HALF_OPEN', // Testing if service recovered
}

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

  constructor(
    private failureThreshold: number = 5,
    private recoveryTimeout: number = 30000,   // 30 seconds
    private halfOpenMaxAttempts: number = 3,
  ) {}

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === CircuitState.OPEN) {
      // Check if enough time has passed to try again
      if (Date.now() - this.lastFailureTime >= this.recoveryTimeout) {
        this.state = CircuitState.HALF_OPEN;
        this.successCount = 0;
      } else {
        throw new Error('Circuit breaker is OPEN - service unavailable');
      }
    }

    try {
      const result = await fn();

      if (this.state === CircuitState.HALF_OPEN) {
        this.successCount++;
        if (this.successCount >= this.halfOpenMaxAttempts) {
          this.state = CircuitState.CLOSED; // Service recovered
          this.failureCount = 0;
        }
      }

      return result;
    } catch (error) {
      this.failureCount++;
      this.lastFailureTime = Date.now();

      if (this.failureCount >= this.failureThreshold) {
        this.state = CircuitState.OPEN;
        console.warn('Circuit breaker tripped - stopping requests to failing service');
      }

      throw error;
    }
  }

  getState(): CircuitState {
    return this.state;
  }
}

// Usage in API gateway
const userServiceBreaker = new CircuitBreaker(5, 30000);

app.get('/api/users/:id', async (req, res) => {
  try {
    const user = await userServiceBreaker.execute(() =>
      fetch(`http://user-service:3001/users/${req.params.id}`).then(r => r.json())
    );
    res.json(user);
  } catch (error) {
    if (error.message.includes('Circuit breaker')) {
      // Return a degraded response or cached data
      res.status(503).json({
        error: 'User service temporarily unavailable',
        retryAfter: 30,
      });
    } else {
      res.status(500).json({ error: 'Internal server error' });
    }
  }
});

API Gateway Solutions Compared

API Gateway Comparison

Feature Kong AWS API Gateway Nginx
TypeOpen source / EnterpriseManaged serviceOpen source
Built onNginx + LuaAWS infrastructureC (custom)
PluginsRich plugin ecosystemLambda authorizersLua/C modules
Rate limitingBuilt-in pluginBuilt-inModule required
AuthJWT, OAuth, LDAP, API keyCognito, Lambda, IAMBasic, JWT (Plus)
MonitoringPrometheus, DatadogCloudWatchAccess logs, stub_status
Best forMulti-cloud, feature-richAWS-native serverlessSimple reverse proxy

API Gateway Best Practices

  • Keep the gateway thin. It should route and apply cross-cutting concerns, not contain business logic. Business logic belongs in the services.
  • Avoid single point of failure. Run multiple gateway instances behind a load balancer or use a managed service.
  • Implement timeouts. Set aggressive timeouts for upstream services to prevent slow services from blocking the gateway.
  • Use circuit breakers. Protect the gateway and healthy services from cascading failures caused by a single failing service.
  • Cache when possible. The gateway can cache responses from upstream services to reduce load and improve latency.
  • Version your APIs. Support multiple API versions simultaneously via path-based (/v1, /v2) or header-based versioning.

Common Anti-Patterns

  • God Gateway: Putting business logic, data transformations, and orchestration in the gateway. This creates a monolith disguised as a gateway.
  • No health checks: Routing traffic to services without knowing if they are healthy leads to cascading failures.
  • Missing correlation IDs: Without a request ID flowing through all services, debugging distributed requests becomes nearly impossible.
  • Ignoring latency: The gateway adds latency to every request. Monitor gateway latency separately from service latency.

Continue Learning