API Gateway: Patterns, Benefits & Implementation
Everything you need to know about API gateways — what they do, core patterns, tool comparison (Kong, AWS, Nginx, Traefik), and building one with Node.js.
What Is an API Gateway?
An API gateway is a server that sits between your clients and your backend services, acting as the single entry point for all API traffic. It intercepts every request, applies cross-cutting policies, and routes traffic to the appropriate upstream service.
Without a gateway:
Mobile App ──► User Service :3001
Web App ──► Order Service :3002 (each client knows all services)
Partner API ──► Payment Service :3003
With a gateway:
Mobile App ┐
Web App ├──► API Gateway :443 ──► User Service :3001
Partner API ┘ ├──► Order Service :3002
└──► Payment Service :3003
Gateway handles: auth, rate limiting, SSL, logging, routing
The gateway centralises concerns that every service would otherwise need to implement independently. It is a foundational pattern in microservices architecture.
Gateway vs Reverse Proxy vs Load Balancer
| Component | Primary Job | API-Specific Features |
|---|---|---|
| Load Balancer (L4) | Distribute TCP connections | None — protocol-agnostic |
| Reverse Proxy (L7) | Forward HTTP requests, SSL termination | Basic URL routing |
| API Gateway (L7+) | All reverse proxy features plus API policies | Auth, rate limiting, transformation, analytics, versioning |
Core Responsibilities
- SSL/TLS termination: Decrypt HTTPS at the gateway; internal services communicate over plain HTTP on a private network
- Authentication & authorization: Validate JWT tokens, API keys, or OAuth tokens once — upstream services receive a verified identity
- Rate limiting: Enforce per-client, per-endpoint, or global request quotas
- Request routing: Map public URLs to internal service URLs (e.g.,
/api/v2/users→user-service:3001/users) - Load balancing: Distribute traffic across multiple instances of a service
- Request/response transformation: Add headers, rewrite paths, translate protocols, aggregate responses
- Logging & tracing: Inject request IDs, capture access logs, forward spans to distributed tracing systems
- Caching: Cache GET responses at the gateway to reduce upstream load
- Circuit breaking: Stop forwarding requests to an unhealthy upstream service
- API versioning: Route
/v1/*to legacy services and/v2/*to new services
Authentication & Authorization at the Gateway
Centralising auth at the gateway means your upstream services can trust the gateway's forwarded identity headers instead of validating tokens themselves — simplifying each service and ensuring consistent enforcement.
// Gateway auth middleware — validates JWT, injects identity headers
const jwt = require('jsonwebtoken');
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'Missing token' });
try {
const payload = jwt.verify(token, process.env.JWT_PUBLIC_KEY, {
algorithms: ['RS256']
});
// Inject verified identity into headers for upstream services
req.headers['x-user-id'] = payload.sub;
req.headers['x-user-roles'] = payload.roles.join(',');
req.headers['x-tenant-id'] = payload.tenant_id;
// Remove the raw token — upstream services don't need it
delete req.headers['authorization'];
next();
} catch (err) {
res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Apply to all routes except public ones
gateway.use((req, res, next) => {
const publicPaths = ['/health', '/auth/login', '/auth/refresh'];
if (publicPaths.some(p => req.path.startsWith(p))) return next();
authMiddleware(req, res, next);
});
API Key Authentication
// API key validation with Redis for performance
const redis = require('redis').createClient();
async function apiKeyMiddleware(req, res, next) {
const key = req.headers['x-api-key'] || req.query.api_key;
if (!key) return res.status(401).json({ error: 'API key required' });
// Check Redis cache first (avoid DB hit on every request)
let client = await redis.get('apikey:' + key);
if (!client) {
// Look up in database
client = await db.apiKeys.findOne({ key, active: true });
if (client) await redis.setEx('apikey:' + key, 300, JSON.stringify(client));
} else {
client = JSON.parse(client);
}
if (!client) return res.status(401).json({ error: 'Invalid API key' });
req.headers['x-client-id'] = client.id;
req.headers['x-client-plan'] = client.plan;
next();
}
Rate Limiting & Throttling
The gateway is the right place to enforce rate limits — it sees all traffic before it reaches your services, and a single Redis instance can track counters across all gateway instances.
// Sliding window rate limiter at the gateway
const redis = require('redis').createClient();
async function rateLimiter(limit, windowMs) {
return async (req, res, next) => {
const key = 'rl:' + (req.headers['x-client-id'] || req.ip);
const now = Date.now();
const win = Math.floor(now / windowMs);
const count = await redis.incr(key + ':' + win);
if (count === 1) await redis.expire(key + ':' + win, Math.ceil(windowMs / 1000) * 2);
res.setHeader('X-RateLimit-Limit', limit);
res.setHeader('X-RateLimit-Remaining', Math.max(0, limit - count));
res.setHeader('X-RateLimit-Reset', (win + 1) * windowMs);
if (count > limit) {
res.setHeader('Retry-After', Math.ceil(windowMs / 1000));
return res.status(429).json({
error: 'Too Many Requests',
retry_after: Math.ceil(windowMs / 1000)
});
}
next();
};
}
// Different limits per plan
gateway.use('/api/', async (req, res, next) => {
const plan = req.headers['x-client-plan'] || 'free';
const limit = { free: 100, pro: 1000, enterprise: 10000 }[plan] || 100;
return rateLimiter(limit, 60_000)(req, res, next); // per minute
});
For a full deep-dive on rate limiting algorithms (token bucket, sliding window, fixed window), see our Rate Limiting Guide.
Routing & Load Balancing
The gateway maps public API paths to internal service URLs, handles path rewriting, and distributes load across service instances.
const { createProxyMiddleware } = require('http-proxy-middleware');
// Route table — maps public paths to internal services
const routes = [
{
path: '/api/v1/users',
target: 'http://user-service:3001',
pathRewrite: { '^/api/v1': '' } // /api/v1/users/42 → /users/42
},
{
path: '/api/v1/orders',
target: 'http://order-service:3002',
pathRewrite: { '^/api/v1': '' }
},
{
path: '/api/v2/users',
target: 'http://user-service-v2:3004', // v2 routes to new service
pathRewrite: { '^/api/v2': '' }
}
];
routes.forEach(({ path, target, pathRewrite }) => {
gateway.use(path, createProxyMiddleware({
target,
changeOrigin: true,
pathRewrite,
on: {
error: (err, req, res) => {
res.status(502).json({ error: 'Service unavailable' });
}
}
}));
});
Weighted Load Balancing
// Canary deploy: send 10% of traffic to v2
function weightedRouter(targets) {
return (req, res, next) => {
const rand = Math.random() * 100;
let cumulative = 0;
for (const t of targets) {
cumulative += t.weight;
if (rand <= cumulative) {
req.target = t.url;
return next();
}
}
};
}
gateway.use('/api/payments', weightedRouter([
{ url: 'http://payment-v1:3003', weight: 90 }, // 90% to stable
{ url: 'http://payment-v2:3005', weight: 10 } // 10% to canary
]));
Request & Response Transformation
Gateways can reshape requests and responses — adding headers, translating formats, or aggregating data from multiple services into a single response.
// Add tracing headers to every upstream request
gateway.use((req, res, next) => {
const { randomUUID } = require('crypto');
req.headers['x-request-id'] = req.headers['x-request-id'] || randomUUID();
req.headers['x-forwarded-for'] = req.ip;
req.headers['x-gateway-time'] = Date.now().toString();
next();
});
// Response transformation — strip internal headers, add versioning
gateway.use((req, res, next) => {
const originalSend = res.json.bind(res);
res.json = (body) => {
// Add API version envelope
originalSend({
data: body,
api_version: '1.0',
request_id: req.headers['x-request-id']
});
};
next();
});
Gateway Comparison: Kong, AWS, Nginx, Traefik
| Gateway | Type | Best For | Auth | Rate Limiting | Cost |
|---|---|---|---|---|---|
| Kong | OSS / Enterprise | Plugin ecosystem, Kubernetes-native, polyglot | JWT, OAuth2, Key Auth plugins | Built-in plugin | Free OSS / Paid Enterprise |
| AWS API Gateway | Managed cloud | AWS Lambda + REST, serverless architectures | Cognito, IAM, Lambda authorizers | Built-in usage plans | Pay-per-request |
| Nginx | OSS reverse proxy | High-performance, custom Lua scripting | Custom (Lua/ngx_http_auth) | limit_req module | Free (OSS) / Paid Plus |
| Traefik | OSS cloud-native | Docker/Kubernetes auto-discovery | Forward auth middleware | Rate limit middleware | Free OSS / Paid Enterprise |
| Envoy Proxy | OSS CNCF | Service mesh, gRPC, advanced load balancing | External auth filter | Built-in local/global | Free (OSS) |
| Azure API Mgmt | Managed cloud | Enterprise, Azure ecosystem, developer portal | OAuth2, subscription keys | Built-in policies | Pay per unit |
Choosing a Gateway
- Kubernetes on any cloud → Kong or Traefik
- AWS + Lambda/serverless → AWS API Gateway
- Maximum performance, custom logic → Nginx + OpenResty (Lua)
- Service mesh + gRPC → Envoy (via Istio or standalone)
- Small team, fast start → Traefik (zero-config with Docker labels)
- Enterprise with developer portal → Kong Enterprise or Azure API Management
Complete Node.js Gateway Example
// gateway.js — production-ready API gateway skeleton
const express = require('express');
const proxy = require('http-proxy-middleware');
const jwt = require('jsonwebtoken');
const redis = require('redis').createClient({ url: process.env.REDIS_URL });
const pino = require('pino');
const logger = pino();
const gateway = express();
await redis.connect();
// 1. Request logging
gateway.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => logger.info({
method: req.method, path: req.path,
status: res.statusCode, ms: Date.now() - start,
requestId: req.headers['x-request-id']
}));
next();
});
// 2. Request ID injection
gateway.use((req, _, next) => {
req.headers['x-request-id'] ||= require('crypto').randomUUID();
next();
});
// 3. JWT auth (skip public paths)
const PUBLIC = new Set(['/health', '/auth/token']);
gateway.use(async (req, res, next) => {
if (PUBLIC.has(req.path)) return next();
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
const p = jwt.verify(token, process.env.JWT_SECRET);
req.headers['x-user-id'] = p.sub;
req.headers['x-user-roles'] = (p.roles || []).join(',');
next();
} catch { res.status(401).json({ error: 'Invalid token' }); }
});
// 4. Rate limiting — 100 req/min per user
gateway.use(async (req, res, next) => {
if (PUBLIC.has(req.path)) return next();
const key = 'rl:' + (req.headers['x-user-id'] || req.ip);
const win = Math.floor(Date.now() / 60000);
const cnt = await redis.incr(key + ':' + win);
if (cnt === 1) await redis.expire(key + ':' + win, 120);
res.setHeader('X-RateLimit-Limit', 100);
res.setHeader('X-RateLimit-Remaining', Math.max(0, 100 - cnt));
if (cnt > 100) return res.status(429).json({ error: 'Rate limit exceeded' });
next();
});
// 5. Route to services
const services = {
'/users': 'http://user-service:3001',
'/orders': 'http://order-service:3002',
'/payments': 'http://payment-service:3003'
};
Object.entries(services).forEach(([path, target]) => {
gateway.use('/api/v1' + path, proxy.createProxyMiddleware({
target, changeOrigin: true,
pathRewrite: { '^/api/v1': '' }
}));
});
// 6. Health check
gateway.get('/health', (_, res) => res.json({ status: 'ok' }));
gateway.listen(8080, () => logger.info('Gateway running on :8080'));
Frequently Asked Questions
Does an API gateway become a single point of failure?
It can — which is why you run multiple gateway instances behind a cloud load balancer and make the gateway stateless (push state to Redis or a distributed cache). All major managed gateways (AWS API Gateway, Kong Cloud) are designed for high availability out of the box. For self-hosted gateways, deploy at least two instances across availability zones.
Should the gateway do business logic?
No. The gateway should handle only cross-cutting infrastructure concerns. Business logic belongs in your services. If you find yourself putting if/else business rules in the gateway, that logic belongs in a dedicated service or the appropriate microservice.
How is an API gateway different from a service mesh?
An API gateway manages north-south traffic — traffic entering your system from clients. A service mesh (Istio, Linkerd) manages east-west traffic — communication between services inside your cluster. They are complementary: the gateway is the front door, the service mesh handles what happens inside. In large Kubernetes deployments, both are used together.
What is a "micro-gateway" or sidecar gateway?
A sidecar gateway runs as a proxy container alongside each service pod (like Envoy in Istio). Instead of a single central gateway, each service has its own proxy that handles auth and rate limiting. This gives more granular control and eliminates the central choke point, but increases operational complexity significantly.
Related Topics
Microservices
REST API design patterns for microservices — BFF, circuit breakers, service-to-service auth.
Authentication
JWT, OAuth 2.0, and API keys — the auth patterns handled at the gateway.
Rate Limiting
Token bucket, sliding window, and fixed window rate limiting algorithms.
Security
OWASP API Top 10, HTTPS, CORS, and security headers enforced at the gateway.
API Monitoring
Metrics, tracing, and health checks — the gateway is a key observability collection point.
gRPC vs REST
Modern gateways support both REST and gRPC — know when to use each.