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: Originor 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.