TechLead
Lesson 10 of 27
5 min read
Software Architecture

API Gateway Pattern

Learn API gateway routing, aggregation, rate limiting, and the Backend for Frontend (BFF) pattern

What is an API Gateway?

An API Gateway is a single entry point for all client requests to a microservices system. It handles cross-cutting concerns like authentication, rate limiting, load balancing, and request routing. Instead of clients calling individual services directly, they call the gateway, which routes requests to the appropriate service.

Gateway Responsibilities

  • Request Routing: Route incoming requests to the appropriate microservice based on path, headers, or content
  • Authentication/Authorization: Verify JWT tokens, API keys, or OAuth tokens before forwarding requests
  • Rate Limiting: Protect services from abuse by limiting request rates per client or API key
  • Response Aggregation: Combine responses from multiple services into a single response for the client
  • Protocol Translation: Convert between REST, GraphQL, gRPC, and WebSocket protocols

Building an API Gateway in TypeScript

// Gateway with Express
import express, { Request, Response, NextFunction } from "express";
import { createProxyMiddleware } from "http-proxy-middleware";

const app = express();

// Service registry
const services: Record = {
  orders: "http://order-service:3001",
  products: "http://product-service:3002",
  users: "http://user-service:3003",
  payments: "http://payment-service:3004",
};

// Authentication middleware
function authenticate(req: Request, res: Response, next: NextFunction): void {
  const token = req.headers.authorization?.replace("Bearer ", "");
  if (!token) {
    res.status(401).json({ error: "No token provided" });
    return;
  }
  try {
    const decoded = verifyJWT(token);
    req.user = decoded;
    next();
  } catch {
    res.status(401).json({ error: "Invalid token" });
  }
}

// Rate limiting middleware
const rateLimits = new Map();

function rateLimit(maxRequests: number, windowMs: number) {
  return (req: Request, res: Response, next: NextFunction): void => {
    const key = req.user?.id || req.ip;
    const now = Date.now();
    const entry = rateLimits.get(key);

    if (!entry || now > entry.resetAt) {
      rateLimits.set(key, { count: 1, resetAt: now + windowMs });
      next();
      return;
    }

    if (entry.count >= maxRequests) {
      res.status(429).json({ error: "Too many requests" });
      return;
    }

    entry.count++;
    next();
  };
}

// Apply middleware
app.use(authenticate);
app.use(rateLimit(100, 60000)); // 100 requests per minute

// Proxy routes to services
for (const [service, target] of Object.entries(services)) {
  app.use(
    `/api/${service}`,
    createProxyMiddleware({
      target,
      changeOrigin: true,
      pathRewrite: { [`^/api/${service}`]: "" },
      onProxyReq: (proxyReq, req) => {
        // Forward user context
        proxyReq.setHeader("X-User-Id", req.user?.id || "");
        proxyReq.setHeader("X-User-Role", req.user?.role || "");
      },
    })
  );
}

app.listen(3000, () => console.log("Gateway running on port 3000"));

Response Aggregation

// Aggregate responses from multiple services
class ResponseAggregator {
  constructor(private readonly httpClient: HttpClient) {}

  async getOrderDetail(orderId: string, userId: string): Promise {
    // Fan out requests to multiple services in parallel
    const [order, customer, products, shipment] = await Promise.allSettled([
      this.httpClient.get(`http://order-service/orders/${orderId}`),
      this.httpClient.get(`http://user-service/users/${userId}`),
      this.httpClient.get(`http://product-service/products?orderId=${orderId}`),
      this.httpClient.get(`http://shipping-service/shipments?orderId=${orderId}`),
    ]);

    return {
      order: order.status === "fulfilled" ? order.value : null,
      customer: customer.status === "fulfilled" ? customer.value : null,
      products: products.status === "fulfilled" ? products.value : [],
      shipment: shipment.status === "fulfilled" ? shipment.value : null,
    };
  }
}

// Gateway endpoint using aggregation
app.get("/api/order-details/:id", async (req, res) => {
  const aggregator = new ResponseAggregator(httpClient);
  const detail = await aggregator.getOrderDetail(req.params.id, req.user.id);
  res.json(detail);
});

Backend for Frontend (BFF)

The Backend for Frontend pattern creates separate API gateways for each frontend type (web, mobile, IoT). Each BFF is tailored to the specific needs of its frontend — the web BFF might return full HTML-ready data while the mobile BFF returns compact JSON.

// Web BFF - rich data for desktop browsers
class WebBFF {
  async getProductPage(productId: string): Promise {
    const [product, reviews, recommendations, relatedProducts] = await Promise.all([
      this.productService.getDetail(productId),
      this.reviewService.getReviews(productId, { limit: 10 }),
      this.recommendationService.getForProduct(productId, { limit: 8 }),
      this.productService.getRelated(productId, { limit: 6 }),
    ]);

    return {
      product,
      reviews,
      recommendations,
      relatedProducts,
      breadcrumbs: this.buildBreadcrumbs(product.category),
      seoMetadata: this.buildSEOMetadata(product),
    };
  }
}

// Mobile BFF - lightweight data for mobile apps
class MobileBFF {
  async getProductPage(productId: string): Promise {
    // Mobile only needs essential data
    const [product, topReviews] = await Promise.all([
      this.productService.getSummary(productId), // Lighter endpoint
      this.reviewService.getTopReviews(productId, { limit: 3 }),
    ]);

    return {
      product: {
        id: product.id,
        name: product.name,
        price: product.price,
        imageUrl: product.thumbnailUrl, // Smaller image
        rating: product.averageRating,
      },
      topReviews,
    };
  }
}

API Gateway Anti-Patterns

  • Business logic in the gateway: Keep the gateway thin — it should route and aggregate, not make business decisions
  • Single point of failure: Deploy the gateway with redundancy and health checks — it is on the critical path
  • Over-aggregation: Do not aggregate everything — if a client only needs data from one service, proxy directly
  • Tight coupling: The gateway should not tightly depend on service internals — use stable API contracts

Continue Learning