API Gateway serves as the single entry point for a group of microservices. This comprehensive guide covers gateway patterns, implementation strategies, security, and operational best practices.
What is an API Gateway?
The Gateway Role
graph TD
Client1[Mobile App] --> Gateway
Client2[Web App] --> Gateway
Client3[Third Party] --> Gateway
Gateway --> Auth[Authentication]
Gateway --> Rate[Rate Limiting]
Gateway --> Cache[Caching]
Gateway --> Route[Routing]
Auth --> Users[/users-service]
Auth --> Orders[/orders-service]
Auth --> Products[/products-service]
Auth --> Payments[/payments-service]
Route --> Cache
Cache --> DB[(Database)]
Gateway Responsibilities
| Responsibility | Description |
|---|---|
| Routing | Forward requests to appropriate microservices |
| Authentication | Verify user identity and tokens |
| Rate Limiting | Prevent abuse and ensure fair usage |
| Caching | Reduce backend load with cached responses |
| Protocol Translation | Convert between HTTP, WebSocket, gRPC |
| Logging/Monitoring | Track requests, latency, errors |
| SSL/TLS Termination | Handle HTTPS connections |
Gateway vs Direct Service Communication
Without API Gateway
graph LR
Client --> Users
Client --> Orders
Client --> Products
Client --> Payments
Users --> Auth1[Auth Service]
Orders --> Auth2[Auth Service]
Products --> Auth3[Auth Service]
Payments --> Auth4[Auth Service]
style Client fill:#f9f
style Users fill:#ff9
style Orders fill:#ff9
style Products fill:#ff9
style Payments fill:#ff9
Problems:
- Each client must know all service locations
- Repeated authentication logic
- No centralized rate limiting
- CORS issues
- Limited caching opportunities
With API Gateway
graph LR
Client --> Gateway[API Gateway]
Gateway --> Auth[Authentication]
Gateway --> Rate[Rate Limiting]
Gateway --> Cache[Caching]
Gateway --> Users
Gateway --> Orders
Gateway --> Products
Gateway --> Payments
style Client fill:#f9f
style Gateway fill:#9f9
Benefits:
- Single entry point for all clients
- Centralized cross-cutting concerns
- Service discovery handled internally
- Simplified client code
- Unified security policy
Popular API Gateway Solutions
Comparison Table
| Gateway | Type | Pros | Cons |
|---|---|---|---|
| Kong | Open Source | Extensive plugins, Lua | Complex setup |
| NGINX | Open Source | High performance, proven | Limited orchestration |
| AWS API Gateway | Managed | Serverless, AWS integration | Vendor lock-in, cost |
| Azure API Management | Managed | Developer portal, analytics | Azure dependency |
| Apigee | Managed | Enterprise features | Complex pricing |
| Traefik | Open Source | Kubernetes native, easy | Less mature |
| Express Gateway | Open Source | Node.js based, JS | Smaller ecosystem |
Kong API Gateway Implementation
Installation
# docker-compose.yml
version: '3.8'
services:
kong:
image: kong:latest
environment:
KONG_DATABASE: postgres
KONG_PG_HOST: postgres
KONG_PG_USER: kong
KONG_PG_PASSWORD: kong
KONG_PROXY_ACCESS_LOG: /dev/stdout
KONG_ADMIN_ACCESS_LOG: /dev/stdout
KONG_PROXY_ERROR_LOG: /dev/stderr
KONG_ADMIN_ERROR_LOG: /dev/stderr
KONG_DECLARATIVE_CONFIG: /kong/kong.yml
ports:
- "8000:8000" # Proxy
- "8443:8443" # Proxy SSL
- "8001:8001" # Admin API
volumes:
- ./kong.yml:/kong/kong.yml
depends_on:
- postgres
networks:
- kong-net
postgres:
image: postgres:13
environment:
POSTGRES_DB: kong
POSTGRES_USER: kong
POSTGRES_PASSWORD: kong
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- kong-net
volumes:
postgres-data:
networks:
kong-net:
driver: bridge
Kong Configuration (declarative)
# kong.yml
_format_version: "3.0"
services:
- name: user-service
url: http://user-service:3001
routes:
- name: user-route
paths:
- /api/users
methods:
- GET
- POST
- PUT
- DELETE
plugins:
- name: jwt
- name: rate-limiting
config:
minute: 100
policy: redis
redis_host: redis
- name: cors
- name: order-service
url: http://order-service:3002
routes:
- name: order-route
paths:
- /api/orders
plugins:
- name: jwt
- name: rate-limiting
config:
minute: 50
- name: cors
- name: product-service
url: http://product-service:3003
routes:
- name: product-route
paths:
- /api/products
plugins:
- name: key-auth
- name: rate-limiting
config:
minute: 200
- name: cors
consumers:
- username: mobile-app
plugins:
- name: rate-limiting
config:
minute: 1000
- username: web-app
plugins:
- name: rate-limiting
config:
minute: 500
- username: trusted-partner
plugins:
- name: rate-limiting
config:
minute: 10000
NGINX API Gateway
Basic Configuration
# /etc/nginx/nginx.conf
http {
upstream user_service {
server user-service-1:3001;
server user-service-2:3002;
keepalive 32;
}
upstream order_service {
server order-service-1:3001;
server order-service-2:3002;
keepalive 32;
}
# Rate limiting zone
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
# API Gateway server
server {
listen 80;
server_name api.example.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Routes
location /api/users {
limit_req zone=api_limit burst=20 nodelay;
# JWT validation
auth_jwt "" token=$http_authorization;
auth_jwt_keyfile /etc/nginx/jwt-key.pem;
proxy_pass http://user_service;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Caching
proxy_cache_valid 200 5m;
proxy_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status;
}
location /api/orders {
limit_req zone=api_limit burst=20 nodelay;
# IP-based access control
allow 10.0.0.0/8;
allow 172.16.0.0/12;
deny all;
proxy_pass http://order_service;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
location /api/products {
# Different rate limit for products
limit_req zone=api_limit burst=50 nodelay;
proxy_pass http://product_service;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Rate limit exceeded
limit_req_status 429;
error_page 429 = @rate_limit_exceeded;
location @rate_limit_exceeded {
return 429 '{"error": "Rate limit exceeded", "retry_after": 60}';
add_header Content-Type application/json;
}
}
}
AWS API Gateway
Serverless Implementation
// AWS Lambda + API Gateway handler
const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();
exports.handler = async (event) => {
const { httpMethod, path, headers, body, queryStringParameters } = event;
// Extract JWT from authorization header
const token = headers.Authorization?.replace('Bearer ', '');
try {
switch (true) {
// GET /users
case httpMethod === 'GET' && path.match(/^\/users$/):
return await handleGetUsers(queryStringParameters);
// GET /users/{id}
case httpMethod === 'GET' && path.match(/^\/users\/(.+)$/):
const userId = path.match(/^\/users\/(.+)$/)[1];
return await handleGetUser(userId);
// POST /users
case httpMethod === 'POST' && path.match(/^\/users$/):
return await handleCreateUser(JSON.parse(body));
// PUT /users/{id}
case httpMethod === 'PUT' && path.match(/^\/users\/(.+)$/):
const updateId = path.match(/^\/users\/(.+)$/)[1];
return await handleUpdateUser(updateId, JSON.parse(body));
// DELETE /users/{id}
case httpMethod === 'DELETE' && path.match(/^\/users\/(.+)$/):
const deleteId = path.match(/^\/users\/(.+)$/)[1];
return await handleDeleteUser(deleteId);
default:
return response(404, { error: 'Not found' });
}
} catch (error) {
console.error('Error:', error);
return response(500, { error: 'Internal server error' });
}
};
async function handleGetUsers(params) {
const { limit = 10, nextToken } = params || {};
const result = await dynamoDB.scan({
TableName: 'users',
Limit: parseInt(limit),
ExclusiveStartKey: nextToken ? JSON.parse(Buffer.from(nextToken, 'base64').toString()) : undefined
}).promise();
return response(200, {
users: result.Items,
nextToken: result.LastEvaluatedKey
? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString('base64')
: null
});
}
async function handleGetUser(userId) {
const result = await dynamoDB.get({
TableName: 'users',
Key: { id: userId }
}).promise();
if (!result.Item) {
return response(404, { error: 'User not found' });
}
return response(200, result.Item);
}
async function handleCreateUser(data) {
const { name, email, role = 'user' } = data;
if (!name || !email) {
return response(400, { error: 'Name and email are required' });
}
const user = {
id: require('crypto').randomUUID(),
name,
email,
role,
createdAt: new Date().toISOString()
};
await dynamoDB.put({
TableName: 'users',
Item: user
}).promise();
return response(201, user);
}
async function handleUpdateUser(userId, data) {
const updateExpressions = [];
const expressionAttributeValues = {};
Object.keys(data).forEach(key => {
if (key !== 'id') {
updateExpressions.push(`${key} = :${key}`);
expressionAttributeValues[`:${key}`] = data[key];
}
});
if (updateExpressions.length === 0) {
return response(400, { error: 'No fields to update' });
}
const result = await dynamoDB.update({
TableName: 'users',
Key: { id: userId },
UpdateExpression: `SET ${updateExpressions.join(', ')}`,
ExpressionAttributeValues: expressionAttributeValues,
ReturnValues: 'ALL_NEW'
}).promise();
return response(200, result.Attributes);
}
async function handleDeleteUser(userId) {
await dynamoDB.delete({
TableName: 'users',
Key: { id: userId }
}).promise();
return response(204, null);
}
function response(statusCode, data) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: data ? JSON.stringify(data) : ''
};
}
AWS API Gateway Terraform
# main.tf
# API Gateway REST API
resource "aws_api_gateway_rest_api" "main" {
name = "my-api-gateway"
description = "Main API Gateway"
endpoint_configuration {
types = ["REGIONAL"]
}
}
# Resource: /users
resource "aws_api_gateway_resource" "users" {
rest_api_id = aws_api_gateway_rest_api.main.id
parent_id = aws_api_gateway_rest_api.main.root_resource_id
path_part = "users"
}
# Method: GET /users
resource "aws_api_gateway_method" "users_get" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.users.id
http_method = "GET"
authorization = "COGNITO_USER_POOLS"
authorizer_id = aws_api_gateway_authorizer.main.id
}
# Integration with Lambda
resource "aws_api_gateway_integration" "users_lambda" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.users.id
http_method = aws_api_gateway_method.users_get.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.main.invoke_arn
}
# Usage Plan
resource "aws_api_gateway_usage_plan" "main" {
name = "api-usage-plan"
api_stages {
api_id = aws_api_gateway_rest_api.main.id
stage = aws_api_gateway_stage.prod.stage_name
}
quota_settings {
limit = 1000000
period = "MONTH"
}
throttle_settings {
burst_limit = 5000
rate_limit = 1000
}
}
# API Key
resource "aws_api_gateway_api_key" "main" {
name = "my-api-key"
}
resource "aws_api_gateway_usage_plan_key" "main" {
key_id = aws_api_gateway_api_key.main.id
key_type = "API_KEY"
usage_plan_id = aws_api_gateway_usage_plan.main.id
}
# Stage
resource "aws_api_gateway_stage" "prod" {
deployment_id = aws_api_gateway_deployment.main.id
rest_api_id = aws_api_gateway_rest_api.main.id
stage_name = "prod"
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api_gateway.arn
format = "$context.requestId: $context.endpoint $context.httpMethod $context.status $context.responseLatency"
}
}
# Deployment
resource "aws_api_gateway_deployment" "main" {
rest_api_id = aws_api_gateway_rest_api.main.id
lifecycle {
create_before_destroy = true
}
}
Authentication Patterns
JWT Validation
// JWT validation middleware (Express.js)
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const jwksUri = process.env.JWKS_URI || 'https://auth.example.com/.well-known/jwks.json';
const audience = process.env.AUDIENCE;
const issuer = process.env.ISSUER;
const client = jwksClient({
jwksUri,
cache: true,
cacheMaxAge: 600000
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
return callback(err);
}
const signingKey = key.getPublicKey();
callback(null, signingKey);
});
}
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
jwt.verify(token, getKey, {
audience,
issuer,
algorithms: ['RS256']
}, (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Invalid token', details: err.message });
}
req.user = decoded;
next();
});
}
// Usage
app.get('/api/protected', authMiddleware, (req, res) => {
res.json({
message: 'Access granted',
user: req.user
});
});
OAuth2 Integration
// OAuth2 token introspection
const axios = require('axios');
async function introspectToken(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'No authorization header' });
}
const token = authHeader.split(' ')[1];
try {
// Introspect with authorization server
const response = await axios.post(
process.env.TOKEN_INTROSPECTION_URL,
`token=${token}`,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${Buffer.from(
process.env.CLIENT_ID + ':' + process.env.CLIENT_SECRET
).toString('base64')}`
}
}
);
const { active, scope, client_id, exp } = response.data;
if (!active) {
return res.status(401).json({ error: 'Token inactive' });
}
// Check expiration
if (exp * 1000 < Date.now()) {
return res.status(401).json({ error: 'Token expired' });
}
req.tokenInfo = { scope, client_id, exp };
next();
} catch (error) {
console.error('Token introspection failed:', error);
return res.status(500).json({ error: 'Token validation failed' });
}
}
// Scope-based access control
function requireScope(...requiredScopes) {
return (req, res, next) => {
const tokenScopes = req.tokenInfo?.scope?.split(' ') || [];
const hasScope = requiredScopes.every(scope => tokenScopes.includes(scope));
if (!hasScope) {
return res.status(403).json({
error: 'Insufficient permissions',
required: requiredScopes,
actual: tokenScopes
});
}
next();
};
}
// Usage
app.get('/api/admin',
requireScope('read:admin', 'write:admin'),
adminHandler
);
Rate Limiting Implementation
Token Bucket Algorithm
// Token bucket rate limiter
class TokenBucket {
constructor(rate, capacity) {
this.rate = rate; // Tokens per second
this.capacity = capacity; // Max tokens in bucket
this.tokens = capacity;
this.lastRefill = Date.now();
}
consume(tokens = 1) {
this.refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return { allowed: true, remaining: this.tokens };
}
const waitTime = ((tokens - this.tokens) / this.rate) * 1000;
return {
allowed: false,
remaining: this.tokens,
retryAfter: Math.ceil(waitTime / 1000)
};
}
refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
const tokensToAdd = elapsed * this.rate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
// Distributed rate limiter with Redis
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function distributedRateLimiter(key, limit, window) {
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, window);
}
const ttl = await redis.ttl(key);
if (current > limit) {
return {
allowed: false,
remaining: 0,
retryAfter: ttl,
limit,
reset: Math.floor(Date.now() / 1000) + ttl
};
}
return {
allowed: true,
remaining: limit - current,
limit,
reset: Math.floor(Date.now() / 1000) + ttl
};
}
// Express middleware
function rateLimitMiddleware(req, res, next) {
const apiKey = req.headers['x-api-key'] || req.ip;
const limit = req.user?.tier === 'premium' ? 1000 : 100;
const window = 60; // seconds
const limiter = new TokenBucket(limit / window, limit);
const result = limiter.consume();
// Set rate limit headers
res.set({
'X-RateLimit-Limit': limit,
'X-RateLimit-Remaining': result.remaining,
'X-RateLimit-Reset': Math.floor(Date.now() / 1000) + window
});
if (!result.allowed) {
res.set('Retry-After', result.retryAfter);
return res.status(429).json({
error: 'Too many requests',
retryAfter: result.retryAfter
});
}
next();
}
Caching Strategies
Multi-layer Caching
// CDN + API Gateway + Service caching
const NodeCache = require('node-cache');
// In-memory cache with TTL
const cache = new NodeCache({ stdTTL: 300 });
// Cache key generator
function cacheKey(method, path, params, userId) {
return `${method}:${path}:${JSON.stringify(params)}:${userId}`;
}
// Cache-aside pattern
async function cacheAside(key, fetchFn, ttl = 300) {
// Check cache first
const cached = cache.get(key);
if (cached) {
return { data: cached, source: 'cache' };
}
// Fetch from source
const data = await fetchFn();
// Store in cache
if (data) {
cache.set(key, data, ttl);
}
return { data, source: 'origin' };
}
// Vary by headers
function varyCacheKey(baseKey, varyHeaders) {
const vary = varyHeaders
.map(h => `${h}:${req.headers[h] || 'none'}`)
.join(':');
return `${baseKey}:${vary}`;
}
// Stale-while-revalidate
async function staleWhileRevalidate(key, fetchFn, ttl = 300) {
const cached = cache.get(key);
if (cached) {
// Return cached immediately
Promise.resolve().then(async () => {
try {
const fresh = await fetchFn();
cache.set(key, fresh, ttl);
} catch (e) {
console.error('Background refresh failed:', e);
}
});
return { data: cached, stale: false };
}
const data = await fetchFn();
cache.set(key, data, ttl);
return { data, stale: false };
}
Circuit Breaker Pattern
// Circuit breaker implementation
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.successThreshold = options.successThreshold || 2;
this.timeout = options.timeout || 60000;
this.state = 'CLOSED';
this.failures = 0;
this.successes = 0;
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
if (this.state === 'HALF_OPEN') {
this.successes++;
if (this.successes >= this.successThreshold) {
this.state = 'CLOSED';
this.successes = 0;
}
}
}
onFailure() {
this.failures++;
this.successes = 0;
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
getState() {
return this.state;
}
}
// Usage with microservices
const userServiceBreaker = new CircuitBreaker({
failureThreshold: 5,
timeout: 30000
});
async function callUserService(endpoint) {
return userServiceBreaker.execute(async () => {
const response = await axios.get(`http://user-service:3001${endpoint}`);
return response.data;
});
}
Monitoring and Observability
Request Logging
// Morgan-style request logging
function loggingMiddleware(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const log = {
timestamp: new Date().toISOString(),
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration,
size: res.get('Content-Length') || 0,
ip: req.ip,
userAgent: req.get('user-agent'),
requestId: req.id || 'unknown'
};
// Send to logging service
console.log(JSON.stringify(log));
// Or send to ELK/Splunk
// elasticsearch.index({ index: 'api-logs', body: log });
});
next();
}
// Metrics collection
const metrics = {
requests: new Counter('api_requests_total', 'Total API requests', ['method', 'endpoint', 'status']),
latency: new Histogram('api_request_duration_ms', 'API request latency', ['method', 'endpoint']),
active: new Gauge('api_active_requests', 'Active API requests')
};
function metricsMiddleware(req, res, next) {
const start = Date.now();
const endpoint = req.route?.path || req.path;
metrics.requests.inc({ method: req.method, endpoint, status: 'started' });
metrics.active.inc({ method: req.method, endpoint });
res.on('finish', () => {
const duration = Date.now() - start;
metrics.requests.inc({ method: req.method, endpoint, status: res.statusCode });
metrics.latency.observe({ method: req.method, endpoint }, duration);
metrics.active.dec({ method: req.method, endpoint });
});
next();
}
External Resources
- Kong Gateway Documentation
- NGINX Gateway Guide
- AWS API Gateway Documentation
- Circuit Breaker Pattern
Comments