Back to Notes

API Gateway

What It Is & Why

A single entry point that sits in front of a microservices backend, so clients talk to one address instead of knowing about (and coupling to) every individual service's location. It's a specialized reverse proxy (see [[Proxies (Forward vs Reverse)]]) that additionally owns cross-cutting concerns every service would otherwise have to reimplement: auth, rate limiting, routing, and observability.

flowchart LR
    C[Client] --> DNS[DNS] --> LB[Load Balancer] --> GW[API Gateway]
    GW --> Auth[Auth Service]
    GW --> RL[Rate Limiter]
    GW --> Cache[(Cache)]
    GW --> S1[User Service]
    GW --> S2[Order Service]
    GW --> S3[Payment Service]

Requirements

Functional: route requests to the correct backend service, authenticate/authorize, rate-limit per client, transform requests/responses (e.g., aggregate multiple backend calls into one client-facing response), load-balance across service instances. Non-functional: low added latency (the gateway is on every request's critical path — budget < 10ms overhead), high availability (99.99%+, since a gateway outage takes down the entire API surface, not one service), horizontally scalable (stateless gateway instances behind their own load balancer).

Key Responsibilities

ConcernTypical implementation
RoutingPath-based (/users/* → user service) or header-based rules
AuthJWT validation, OAuth2 token introspection, API keys — see [[API Design Principles]]
Rate limitingToken bucket per client/endpoint — see [[Rate Limiter]]
CachingRedis-backed cache for cacheable GET responses — see [[Caching & Redis]]
Request/response transformationReshape payloads, aggregate multiple backend calls into one response (avoids the client making N round trips)
Circuit breakingFail fast when a downstream service is unhealthy, instead of piling up timeouts
ObservabilityRequest logging, distributed tracing (propagate a trace ID through every downstream call)

API Gateway vs. Reverse Proxy vs. Service Mesh — a distinction worth stating precisely

  • Reverse proxy (nginx, HAProxy): routes and load-balances traffic. Generic — doesn't know about your specific business API contracts.
  • API Gateway (Kong, AWS API Gateway, Envoy Gateway): a reverse proxy specialized for API traffic — adds auth, rate limiting, request transformation, and API-specific observability on top of routing.
  • Service mesh (Istio, Linkerd, via sidecar proxies): handles service-to-service traffic inside the backend (between microservices), not client-to-backend. Solves mTLS, retries, and observability for internal calls. An API Gateway and a service mesh are complementary, not competing — the gateway is the north-south (client↔backend) front door; the mesh governs east-west (service↔service) traffic.

The Backend-for-Frontend (BFF) Pattern

Instead of one generic gateway serving every client type identically, run a thin gateway layer per client type (mobile BFF, web BFF) — each shaped to that client's specific data needs (a mobile client might want a smaller aggregated payload than a web dashboard). Avoids a single gateway accumulating conditional logic for every consumer.

Real Products (name-recognition matters in interviews)

  • Kong, AWS API Gateway, Apigee — managed/self-hosted API gateway products with plugin ecosystems for auth/rate-limiting/transformation.
  • Envoy — high-performance proxy used both as an API gateway and as the sidecar data plane in service meshes (Istio is built on it).
  • Netflix Zuul — an early, widely-cited API gateway design (JVM-based, filter-chain architecture) — useful historical reference point.

Minimal Illustration — Routing + Auth Middleware

from functools import wraps

ROUTES = {
    "/users": "http://user-service:8001",
    "/orders": "http://order-service:8002",
}

def require_auth(handler):
    @wraps(handler)
    def wrapper(request):
        token = request.headers.get("Authorization")
        if not token or not verify_jwt(token):          # signature + expiry check
            return {"status": 401, "body": "unauthorized"}
        return handler(request)
    return wrapper

@require_auth
def route_request(request):
    for prefix, upstream in ROUTES.items():
        if request.path.startswith(prefix):
            return forward(upstream, request)            # proxy to the matched service
    return {"status": 404, "body": "no route"}

This is the conceptual core — production gateways add connection pooling, circuit breakers, retries with backoff, and per-route rate-limit rules around this same routing skeleton.

Interview Drills

  1. Why not let each microservice handle its own auth? — Duplicated logic across every service, inconsistent enforcement, and every service now needs to know about token validation/rotation. Centralizing at the gateway means one place to update, audit, and secure.
  2. The gateway goes down. What happens? — Total API outage, since it's the sole entry point — this is exactly why its non-functional requirement (99.99%+ availability, horizontal scaling, no single instance) is stricter than almost any individual backend service.
  3. How do you avoid the gateway becoming a monolith of routing logic as services grow? — BFF pattern (split gateways per client type) and/or push cross-cutting logic into gateway plugins (Kong-style) rather than hand-rolled conditionals in one codebase.

Cross-links

[[Proxies (Forward vs Reverse)]] · [[API Design Principles]] · [[Rate Limiter]] · [[Load Balancing]] · [[Caching & Redis]]