API Gateways have become essential components in modern distributed systems. They act as the single entry point for all client requests, handling cross-cutting concerns like authentication, rate limiting, routing, and composition. This comprehensive guide explores API Gateway patterns and implementation strategies.
What is an API Gateway?
An API Gateway sits between clients and backend services, providing a unified API interface while abstracting backend complexity. It handles request routing, composition, and protocol translation, allowing microservices to remain loosely coupled.
Core Responsibilities
- 1Request Routing: Direct requests to appropriate backend services
- 2Authentication & Authorization: Validate and enrich requests with user context
- 3Rate Limiting: Prevent abuse and ensure fair usage
- 4Load Balancing: Distribute traffic across service instances
- 5Request/Response Transformation: Convert between protocols and formats
- 6Caching: Reduce backend load with intelligent caching
- 7Monitoring & Logging: Track all API traffic for observability
Gateway Patterns
Pattern 1: Basic Routing Gateway
The simplest pattern routes requests based on URL paths:
// gateway/routes.ts
import { Router } from 'express';
const router = Router();
router.use('/users', (req, res, next) => {
proxyRequest(req, res, 'http://user-service:3001');
});
router.use('/orders', (req, res, next) => {
proxyRequest(req, res, 'http://order-service:3002');
});
router.use('/products', (req, res, next) => {
proxyRequest(req, res, 'http://product-service:3003');
});
function proxyRequest(req, res, targetUrl) {
const options = {
method: req.method,
headers: req.headers,
body: req.body
};
fetch(`${targetUrl}${req.path}`, options)
.then(response => {
res.status(response.status);
response.body.pipe(res);
})
.catch(err => {
res.status(502).json({ error: 'Bad Gateway' });
});
}
export default router;Pattern 2: Aggregation Gateway
Combine multiple backend calls into a single response:
// gateway/aggregator.ts
interface UserProfile {
id: string;
name: string;
}
interface UserOrders {
userId: string;
orders: Order[];
}
interface UserRecommendations {
userId: string;
products: Product[];
}
async function getUserDashboard(userId: string) {
const [user, orders, recommendations] = await Promise.all([
fetch(`http://user-service/${userId}`).then(r => r.json()),
fetch(`http://order-service/user/${userId}`).then(r => r.json()),
fetch(`http://recommendation-service/${userId}`).then(r => r.json())
]);
return {
user,
orders,
recommendations
};
}
gateway.get('/dashboard/:userId', async (req, res) => {
try {
const dashboard = await getUserDashboard(req.params.userId);
res.json(dashboard);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch dashboard' });
}
});Pattern 3: BFF (Backend for Frontend) Gateway
Create specialized gateways for different client types:
// gateway/mobile-bff.ts
// Optimized for mobile clients
class MobileBFF {
async getProducts(req, res) {
// Mobile needs fewer fields
const products = await productService.getAll();
const mobileProducts = products.map(p => ({
id: p.id,
name: p.name,
price: p.price,
thumbnail: p.images[0]
}));
res.json(mobileProducts);
}
}
// gateway/web-bff.ts
// Optimized for web clients
class WebBFF {
async getProducts(req, res) {
// Web needs full details
const products = await productService.getAll();
res.json(products);
}
}Authentication Patterns
JWT Validation
// gateway/auth.ts
import jwt from 'jsonwebtoken';
interface JwtPayload {
userId: string;
roles: string[];
}
async function validateToken(token: string): Promise<JwtPayload | null> {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET) as JwtPayload;
return payload;
} catch (error) {
return null;
}
}
export async function authMiddleware(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
const payload = await validateToken(token);
if (!payload) {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = payload;
next();
}OAuth 2.0 Integration
// gateway/oauth.ts
import axios from 'axios';
async function exchangeCodeForToken(code: string) {
const response = await axios.post('https://oauth-provider.com/token', {
grant_type: 'authorization_code',
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
code,
redirect_uri: process.env.OAUTH_REDIRECT_URI
});
return response.data;
}
async function refreshAccessToken(refreshToken: string) {
const response = await axios.post('https://oauth-provider.com/token', {
grant_type: 'refresh_token',
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
refresh_token: refreshToken
});
return response.data;
}Rate Limiting Strategies
Token Bucket Algorithm
// gateway/rate-limiter.ts
class TokenBucket {
private tokens: number;
private lastRefill: number;
constructor(
private capacity: number,
private refillRate: number // tokens per second
) {
this.tokens = capacity;
this.lastRefill = Date.now();
}
consume(tokens = 1): boolean {
this.refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return true;
}
return false;
}
private refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
const tokensToAdd = elapsed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
class RateLimiter {
private buckets = new Map<string, TokenBucket>();
constructor(
private capacity: number,
private refillRate: number
) {}
isAllowed(identifier: string): boolean {
if (!this.buckets.has(identifier)) {
this.buckets.set(
identifier,
new TokenBucket(this.capacity, this.refillRate)
);
}
return this.buckets.get(identifier)!.consume();
}
}
export const rateLimiter = new RateLimiter(100, 10); // 100 tokens, 10 per secondSliding Window Log
class SlidingWindowLog {
private logs = new Map<string, number[]>();
isAllowed(identifier: string, limit: number, windowMs: number): boolean {
const now = Date.now();
const windowStart = now - windowMs;
if (!this.logs.has(identifier)) {
this.logs.set(identifier, []);
}
const userLogs = this.logs.get(identifier)!;
// Remove old entries
const validLogs = userLogs.filter(timestamp => timestamp > windowStart);
this.logs.set(identifier, validLogs);
if (validLogs.length < limit) {
validLogs.push(now);
return true;
}
return false;
}
}Service Discovery
Dynamic service discovery enables resilient routing:
// gateway/service-discovery.ts
interface ServiceInstance {
host: string;
port: number;
lastHealthCheck: number;
}
class ServiceRegistry {
private services = new Map<string, ServiceInstance[]>();
register(serviceName: string, instance: ServiceInstance) {
if (!this.services.has(serviceName)) {
this.services.set(serviceName, []);
}
this.services.get(serviceName)!.push(instance);
}
discover(serviceName: string): ServiceInstance | null {
const instances = this.services.get(serviceName);
if (!instances || instances.length === 0) {
return null;
}
// Round-robin selection
const now = Date.now();
const healthyInstances = instances.filter(
i => now - i.lastHealthCheck < 30000 // Last check within 30s
);
if (healthyInstances.length === 0) {
return null;
}
const index = Math.floor(Math.random() * healthyInstances.length);
return healthyInstances[index];
}
}
export const serviceRegistry = new ServiceRegistry();Circuit Breaker Pattern
Prevent cascading failures with circuit breakers:
// gateway/circuit-breaker.ts
enum CircuitState {
CLOSED,
OPEN,
HALF_OPEN
}
class CircuitBreaker {
private state = CircuitState.CLOSED;
private failureCount = 0;
private lastFailureTime = 0;
private successCount = 0;
constructor(
private threshold: number,
private timeout: number,
private halfOpenAttempts: number
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = CircuitState.HALF_OPEN;
this.successCount = 0;
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.halfOpenAttempts) {
this.state = CircuitState.CLOSED;
this.failureCount = 0;
}
} else {
this.failureCount = 0;
}
}
private onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.threshold) {
this.state = CircuitState.OPEN;
}
}
}Caching Strategies
Response Caching
// gateway/cache.ts
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 300 }); // 5 minute default
export function cacheMiddleware(ttl?: number) {
return (req, res, next) => {
const key = `${req.method}:${req.originalUrl}`;
const cached = cache.get(key);
if (cached) {
return res.json(cached);
}
// Store original res.json
const originalJson = res.json.bind(res);
// Override res.json to cache responses
res.json = function(data) {
cache.set(key, data, ttl);
return originalJson(data);
};
next();
};
}Request Transformation
Convert between different API versions:
// gateway/transformer.ts
interface V1User {
id: string;
first_name: string;
last_name: string;
}
interface V2User {
id: string;
name: {
first: string;
last: string;
};
}
function transformV1ToV2(v1User: V1User): V2User {
return {
id: v1User.id,
name: {
first: v1User.first_name,
last: v1User.last_name
}
};
}
gateway.get('/v2/users/:id', async (req, res) => {
const v1Response = await fetch(
`http://user-service/v1/users/${req.params.id}`
);
const v1User = await v1Response.json();
const v2User = transformV1ToV2(v1User);
res.json(v2User);
});Monitoring and Observability
Comprehensive monitoring is essential:
// gateway/monitoring.ts
import Prometheus from 'prom-client';
const httpRequestDuration = new Prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'route', 'status_code']
});
export function monitoringMiddleware(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
httpRequestDuration.observe(
{
method: req.method,
route: req.route?.path || req.path,
status_code: res.statusCode
},
duration
);
});
next();
}Conclusion
API Gateways are critical components in modern microservices architectures. By implementing the patterns and strategies in this guide, you can build robust, scalable gateways that handle cross-cutting concerns effectively while keeping your services loosely coupled.
Remember to start simple and add complexity only when needed. Over-engineering your gateway can become a bottleneck, so always measure and iterate based on real requirements.