caduh

What is an API Gateway? — and why you might need one

4 min read

A simple explainer of API gateways—how they centralize auth, rate limiting, routing, and observability—and when to adopt one versus calling services directly.

TL;DR

  • An API gateway is the single entry point between clients and your services. It offloads auth, rate limiting, routing, CORS, TLS, caching, and observability.
  • Use a gateway for external (north–south) traffic; use a service mesh for internal (east–west) traffic.
  • Start with JWT/OAuth2, per‑client rate limits, and path‑based routing. Keep business logic out of the gateway.
  • Choose managed (AWS API Gateway, Azure APIM, Cloudflare) for speed, or self‑hosted (Kong, NGINX, Traefik, Envoy) for control.
  • Treat config as code, version it, and test limits in staging before flipping the switch.

Why have a gateway at all?

Without a gateway, every service must implement the same cross‑cutting concerns: auth, throttling, CORS, input validation, logging/metrics, TLS, IP allow/deny lists. That leads to duplication, drift, and inconsistent security. A gateway centralizes these at the edge so your services focus on business logic.

Client ──► API Gateway (auth, rate limit, route, transform, observe) ──► Services

What an API gateway actually does

  • Authenticate & authorize: Validate JWTs, verify OAuth2 tokens, check scopes/roles, map identities to headers.
  • Rate limit & quota: Per key/IP/user throttles, burst control, and request size limits to stop abuse.
  • Routing & versioning: Path/host based routing (/v1/* → svc A, /v2/* → svc B), canary/blue‑green rollouts.
  • Transformations: Rewrite paths/headers, shape payloads, validate JSON (Schema), sometimes aggregate responses.
  • CORS & TLS: Terminate TLS, enforce HSTS, answer preflights; optionally speak mTLS upstream.
  • Caching: Short‑TTL cache for idempotent GETs to reduce origin load.
  • Observability: Structured logs, metrics, and trace propagation (traceparent, x-request-id).

When you probably need one

  • You’re exposing multiple services to the public internet.
  • You require consistent authentication (e.g., OAuth for mobile/web) and standardized rate limits.
  • You want single‑place API keys, quotas, and analytics/monetization.
  • You’re moving to microservices and don’t want each team re‑implementing edge security.

When you might not

  • A single internal app behind a firewall/CDN.
  • A small MVP where direct calls are simpler. (Add a gateway later—don’t over‑engineer day 1.)

Minimal, real‑world setups

1) Self‑hosted with Kong (declarative)

_format_version: "3.0"
services:
  - name: users-v1
    url: http://users.default.svc.cluster.local:8080
    routes:
      - name: users-route
        paths: ["/v1/users"]
        methods: ["GET","POST"]
plugins:
  - name: jwt           # Validate Bearer tokens (RS256 via JWKS)
  - name: rate-limiting
    config: { minute: 100, policy: local }
  - name: cors
    config:
      origins: ["https://app.example.com"]
      methods: ["GET","POST","OPTIONS"]
      headers: ["Authorization","Content-Type"]
      credentials: true
      max_age: 600

2) NGINX (path routing + rate limit)

http {
  # 100 requests/min per IP, burst 20
  limit_req_zone $binary_remote_addr zone=perip:10m rate=100r/m;

  server {
    listen 443 ssl http2;
    server_name api.example.com;
    # ssl_certificate ...; ssl_certificate_key ...;

    # CORS (simplified example)
    if ($request_method = OPTIONS) {
      add_header Access-Control-Allow-Origin "https://app.example.com" always;
      add_header Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS" always;
      add_header Access-Control-Allow-Headers "Authorization,Content-Type" always;
      add_header Access-Control-Max-Age 600 always;
      return 204;
    }
    add_header Access-Control-Allow-Origin "https://app.example.com" always;
    add_header Vary "Origin" always;

    location /v1/ {
      limit_req zone=perip burst=20 nodelay;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_pass http://users-svc:8080/;
    }
  }
}

3) Tiny Node “edge” with auth + proxy

import express from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
import jwt from "jsonwebtoken";

const app = express();
const upstream = "http://users:8080";

function requireJWT(req, res, next) {
  try {
    const [, token] = (req.headers.authorization || "").split(" ");
    const claims = jwt.verify(token, process.env.JWKS_PUBLIC_KEY, { algorithms: ["RS256"] });
    // pass identity to upstream
    req.headers["x-user-id"] = claims.sub;
    next();
  } catch (e) {
    return res.status(401).json({ error: "invalid_token" });
  }
}

app.use("/v1", requireJWT, createProxyMiddleware({ target: upstream, changeOrigin: true }));
app.listen(8080);

BFF vs Gateway vs Service Mesh (quick compare)

| Pattern | Traffic | Best for | Notes | |---|---|---|---| | API Gateway | North–south | Central auth, rate limiting, routing, public APIs | Keep thin; config as code | | BFF (Backend‑for‑Frontend) | North–south | Tailored endpoints per UI (web/mobile) | Often sits behind the gateway | | Service Mesh | East–west | mTLS, retries, timeouts between services | Complements a gateway; no public ingress |


Gotchas to avoid

  • Business logic in the gateway → hard to test/deploy; keep it at the service.
  • Forgetting Vary: Origin or client IP propagation (X-Forwarded-For).
  • Using wildcard CORS with credentials (blocked by browsers).
  • Not rate‑limiting by API key/user (IP‑only is weak behind NATs/CDNs).
  • Skipping request size limits and timeouts/circuit breakers.

Quick checklist

  • [ ] TLS at the edge, HSTS, redirect HTTP→HTTPS.
  • [ ] JWT/OAuth2 validation; pass identity via headers.
  • [ ] Per‑client rate limits & quotas; protect uploads with size/time limits.
  • [ ] Path/host routing with versioning and canaries.
  • [ ] CORS configured (and Vary: Origin).
  • [ ] Structured logs, metrics, tracing headers; dashboards & alerts.
  • [ ] Config as code, reviewed and tested in staging.

One‑minute decision guide

  • Public API or many clients? Use a gateway.
  • One internal service? You can defer; a reverse proxy may suffice.
  • Microservices + internal traffic? Gateway plus a service mesh later.
  • Need productized APIs/monetization? Choose a managed gateway first.