Understanding API Gateway Patterns
An API Gateway is a server that acts as an entry point for client applications to access backend services. It sits between clients and microservices, providing a unified interface while handling cross-cutting concerns like authentication, rate limiting, load balancing, and request routing. API Gateways are essential for managing complex distributed architectures.
What is an API Gateway?
An API Gateway is a reverse proxy that accepts API calls from clients, aggregates the various services required to fulfill them, and returns the appropriate result. Think of it as a traffic cop for your API ecosystem—it directs requests to the right services and enforces policies.
Key Concept: The API Gateway pattern implements the "Backend for Frontend" (BFF) pattern, where different clients (web, mobile, IoT) can have customized gateway implementations tailored to their specific needs.
Core Responsibilities of an API Gateway
1. Request Routing
The gateway maps incoming requests to appropriate backend services based on URL patterns, headers, or other criteria.
# Nginx routing configuration
location /api/users/ {
proxy_pass http://user-service:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/products/ {
proxy_pass http://product-service:3001/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/orders/ {
proxy_pass http://order-service:3002/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
2. Authentication and Authorization
Centralize authentication logic at the gateway level to avoid duplicating security code across services.
// Express.js API Gateway authentication
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
// Authentication middleware
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
};
// Apply authentication to all /api routes
app.use('/api', authenticateToken);
// Route to user service
app.use('/api/users', createProxyMiddleware({
target: 'http://user-service:3000',
changeOrigin: true,
pathRewrite: { '^/api/users': '/' },
onProxyReq: (proxyReq, req) => {
// Forward user info to backend services
proxyReq.setHeader('X-User-Id', req.user.id);
proxyReq.setHeader('X-User-Role', req.user.role);
}
}));
Best Practice: Use JWT tokens with short expiration times and implement token refresh mechanisms. Store sensitive operations (like user roles) in the token payload to avoid additional database lookups.
Load Balancing Strategies
API Gateways distribute incoming requests across multiple instances of backend services to ensure high availability and optimal performance.
Common Load Balancing Algorithms
1. Round Robin: Distributes requests sequentially across available servers.
# Nginx round-robin load balancing
upstream user_service {
server user-service-1:3000;
server user-service-2:3000;
server user-service-3:3000;
}
location /api/users/ {
proxy_pass http://user_service/;
}
2. Least Connections: Routes requests to the server with the fewest active connections.
upstream user_service {
least_conn;
server user-service-1:3000;
server user-service-2:3000;
server user-service-3:3000;
}
3. IP Hash: Ensures the same client always reaches the same server (useful for sticky sessions).
upstream user_service {
ip_hash;
server user-service-1:3000;
server user-service-2:3000;
server user-service-3:3000;
}
4. Weighted Load Balancing: Assigns more traffic to powerful servers.
upstream user_service {
server user-service-1:3000 weight=3; # Gets 60% of traffic
server user-service-2:3000 weight=2; # Gets 40% of traffic
}
Health Checks: Always implement health checks to automatically remove unhealthy servers from the load balancing pool. Nginx can perform passive health checks, while Nginx Plus offers active health checks.
Request and Response Transformation
Gateways can modify requests before forwarding them and transform responses before returning to clients.
// Express.js request/response transformation
const axios = require('axios');
app.get('/api/products/:id', authenticateToken, async (req, res) => {
try {
// Fetch from multiple services
const [product, reviews, inventory] = await Promise.all([
axios.get(`http://product-service:3001/products/${req.params.id}`),
axios.get(`http://review-service:3002/reviews?productId=${req.params.id}`),
axios.get(`http://inventory-service:3003/inventory/${req.params.id}`)
]);
// Aggregate and transform response
const aggregatedResponse = {
id: product.data.id,
name: product.data.name,
price: product.data.price,
description: product.data.description,
averageRating: calculateAverageRating(reviews.data),
reviewCount: reviews.data.length,
inStock: inventory.data.quantity > 0,
stockQuantity: inventory.data.quantity,
// Add computed fields
isOnSale: product.data.salePrice < product.data.price,
savings: product.data.price - product.data.salePrice
};
res.json(aggregatedResponse);
} catch (error) {
console.error('Gateway error:', error);
res.status(500).json({ error: 'Failed to fetch product details' });
}
});
function calculateAverageRating(reviews) {
if (reviews.length === 0) return 0;
const sum = reviews.reduce((acc, review) => acc + review.rating, 0);
return (sum / reviews.length).toFixed(1);
}
Rate Limiting and Throttling
Protect backend services from overload by implementing rate limiting at the gateway level.
// Express rate limiting with redis
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
const redis = new Redis({
host: 'localhost',
port: 6379
});
// Create rate limiter
const apiLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rate_limit:',
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests from this IP, please try again later.'
},
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
});
// Apply rate limiting to API routes
app.use('/api/', apiLimiter);
// Different limits for different endpoints
const strictLimiter = rateLimit({
store: new RedisStore({ client: redis }),
windowMs: 15 * 60 * 1000,
max: 10, // Stricter limit for sensitive endpoints
});
app.use('/api/auth/login', strictLimiter);
app.use('/api/auth/register', strictLimiter);
# Nginx rate limiting
# Define rate limit zone (10MB can store ~160,000 IP addresses)
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
location /api/ {
# Apply rate limit (allow bursts of 20 requests)
limit_req zone=api_limit burst=20 nodelay;
# Custom error response
limit_req_status 429;
proxy_pass http://backend_services;
}
Popular API Gateway Solutions
Kong API Gateway
Kong is a cloud-native, platform-agnostic API gateway built on top of Nginx. It offers extensive plugin ecosystem and enterprise features.
# Kong configuration example (declarative config)
_format_version: "2.1"
services:
- name: user-service
url: http://user-service:3000
routes:
- name: user-routes
paths:
- /api/users
plugins:
- name: rate-limiting
config:
minute: 100
policy: local
- name: jwt
config:
secret_is_base64: false
- name: cors
config:
origins:
- "*"
methods:
- GET
- POST
- PUT
- DELETE
headers:
- Authorization
- Content-Type
- name: product-service
url: http://product-service:3001
routes:
- name: product-routes
paths:
- /api/products
plugins:
- name: response-transformer
config:
add:
headers:
- X-Service:product-service
Kong Advantages: Plugin architecture, built-in authentication/authorization, declarative configuration, service mesh capabilities, and excellent performance. It's particularly well-suited for microservices architectures.
AWS API Gateway
Fully managed service that makes it easy to create, publish, maintain, monitor, and secure APIs at any scale.
// AWS API Gateway with Lambda integration (Serverless Framework)
service: api-gateway-example
provider:
name: aws
runtime: nodejs18.x
region: us-east-1
functions:
getUsers:
handler: handlers/users.get
events:
- http:
path: users
method: get
cors: true
authorizer:
name: authFunction
resultTtlInSeconds: 300
createUser:
handler: handlers/users.create
events:
- http:
path: users
method: post
cors: true
authorizer:
name: authFunction
authFunction:
handler: handlers/auth.authorize
resources:
Resources:
ApiGatewayRestApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: MyAPI
# API throttling settings
ApiGatewayUsagePlan:
Type: AWS::ApiGateway::UsagePlan
Properties:
UsagePlanName: BasicPlan
Throttle:
BurstLimit: 200
RateLimit: 100
Circuit Breaker Pattern with API Gateway
Prevent cascading failures by implementing circuit breakers at the gateway level.
// Circuit breaker implementation using opossum
const CircuitBreaker = require('opossum');
const axios = require('axios');
// Function to call backend service
async function callUserService(userId) {
const response = await axios.get(`http://user-service:3000/users/${userId}`);
return response.data;
}
// Circuit breaker options
const options = {
timeout: 3000, // If function takes longer than 3s, trigger failure
errorThresholdPercentage: 50, // When 50% of requests fail, open circuit
resetTimeout: 10000, // After 10s, try again (half-open state)
volumeThreshold: 10, // Minimum number of requests before tripping
};
// Create circuit breaker
const breaker = new CircuitBreaker(callUserService, options);
// Handle circuit events
breaker.on('open', () => {
console.log('Circuit breaker opened - service is down');
});
breaker.on('halfOpen', () => {
console.log('Circuit breaker half-open - testing service');
});
breaker.on('close', () => {
console.log('Circuit breaker closed - service is healthy');
});
// Use in Express route
app.get('/api/users/:id', async (req, res) => {
try {
const user = await breaker.fire(req.params.id);
res.json(user);
} catch (error) {
if (breaker.opened) {
// Circuit is open, return cached data or fallback
res.status(503).json({
error: 'Service temporarily unavailable',
cached: true
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
Warning: Circuit breakers should be configured carefully. Setting thresholds too low can cause false positives, while setting them too high defeats the purpose of having a circuit breaker.
Request Aggregation and Composition
API Gateways can combine multiple backend calls into a single client request, reducing network overhead.
// GraphQL-style aggregation in REST API Gateway
app.get('/api/dashboard/:userId', authenticateToken, async (req, res) => {
const { userId } = req.params;
try {
// Parallel requests to multiple services
const [user, orders, recommendations, notifications] = await Promise.allSettled([
axios.get(`http://user-service:3000/users/${userId}`),
axios.get(`http://order-service:3001/orders?userId=${userId}&limit=5`),
axios.get(`http://recommendation-service:3002/recommendations/${userId}`),
axios.get(`http://notification-service:3003/notifications/${userId}?unread=true`)
]);
// Build aggregated response
const dashboard = {
user: user.status === 'fulfilled' ? user.value.data : null,
recentOrders: orders.status === 'fulfilled' ? orders.value.data : [],
recommendations: recommendations.status === 'fulfilled' ? recommendations.value.data : [],
unreadNotifications: notifications.status === 'fulfilled' ? notifications.value.data : [],
// Add metadata
loadedAt: new Date().toISOString(),
partialFailure: [user, orders, recommendations, notifications].some(r => r.status === 'rejected')
};
res.json(dashboard);
} catch (error) {
console.error('Dashboard aggregation error:', error);
res.status(500).json({ error: 'Failed to load dashboard' });
}
});
Caching at the Gateway Level
Implement response caching to reduce backend load and improve performance.
// Redis-based response caching
const redis = require('redis');
const client = redis.createClient({ url: 'redis://localhost:6379' });
client.on('error', err => console.error('Redis error:', err));
await client.connect();
// Caching middleware
const cacheMiddleware = (duration) => {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
try {
// Check cache
const cachedResponse = await client.get(key);
if (cachedResponse) {
console.log('Cache hit:', key);
return res.json(JSON.parse(cachedResponse));
}
// Override res.json to cache response
const originalJson = res.json.bind(res);
res.json = (data) => {
// Cache the response
client.setEx(key, duration, JSON.stringify(data))
.catch(err => console.error('Cache set error:', err));
return originalJson(data);
};
next();
} catch (error) {
console.error('Cache middleware error:', error);
next(); // Continue without caching on error
}
};
};
// Apply caching to specific routes
app.get('/api/products', cacheMiddleware(300), async (req, res) => {
const response = await axios.get('http://product-service:3001/products');
res.json(response.data);
});
Exercise: Design an API Gateway for an e-commerce platform with the following requirements:
- Three backend services: User Service (port 3000), Product Service (port 3001), Order Service (port 3002)
- Implement JWT authentication for all routes
- Apply rate limiting: 100 requests per 15 minutes for general endpoints, 10 requests per 15 minutes for authentication endpoints
- Create an aggregated endpoint /api/checkout/:orderId that fetches order details, user information, and product information in a single request
- Implement response caching for product listings (5 minutes TTL)
- Add health check endpoints for each backend service
Choose either Nginx, Kong, or Express.js and provide the complete configuration.