Introduction to API Gateways
An API Gateway serves as a single entry point for all clients into a microservices architecture. It sits between clients and backend services, routing requests, aggregating responses, and handling cross-cutting concerns.
Analogy: Hotel Concierge
An API Gateway is like a hotel concierge:
- Single point of contact: Guests don't need to know which staff member handles what - they just ask the concierge
- Request routing: The concierge directs requests to the appropriate department (housekeeping, room service, maintenance)
- Aggregation: For complex requests ("I need dinner reservations, tickets to a show, and a taxi"), the concierge coordinates with multiple services and presents a unified response
- Protocol translation: Guests speak their language, and the concierge translates to what each department needs
- Security: The concierge verifies that guests are actually staying at the hotel before providing certain services
Key Functions of an API Gateway
API Gateways serve multiple important functions in a microservices architecture:
Request Routing
Directing client requests to the appropriate backend service.
// Example routing configuration in Express Gateway
module.exports = {
http: {
port: 8080
},
apiEndpoints: {
users: {
host: 'localhost',
paths: '/users/*'
},
products: {
host: 'localhost',
paths: ['/products', '/products/*', '/categories/*']
},
orders: {
host: 'localhost',
paths: ['/orders', '/orders/*', '/cart']
}
},
serviceEndpoints: {
userService: {
url: 'http://localhost:3001'
},
productService: {
url: 'http://localhost:3002'
},
orderService: {
url: 'http://localhost:3003'
}
},
policies: ['proxy'],
pipelines: {
userPipeline: {
apiEndpoints: ['users'],
policies: [
{
proxy: {
action: {
serviceEndpoint: 'userService',
changeOrigin: true,
stripPath: false
}
}
}
]
},
productPipeline: {
apiEndpoints: ['products'],
policies: [
{
proxy: {
action: {
serviceEndpoint: 'productService',
changeOrigin: true,
stripPath: false
}
}
}
]
},
orderPipeline: {
apiEndpoints: ['orders'],
policies: [
{
proxy: {
action: {
serviceEndpoint: 'orderService',
changeOrigin: true,
stripPath: false
}
}
}
]
}
}
};
Request Aggregation
Combining results from multiple backend services into a single response.
// Example of request aggregation in Node.js with Express
const express = require('express');
const axios = require('axios');
const app = express();
app.get('/product-details/:id', async (req, res) => {
try {
const productId = req.params.id;
// Parallel requests to multiple services
const [productResponse, reviewsResponse, inventoryResponse] = await Promise.all([
axios.get(`http://product-service/products/${productId}`),
axios.get(`http://review-service/reviews?productId=${productId}`),
axios.get(`http://inventory-service/inventory/${productId}`)
]);
// Aggregate the responses
const aggregatedResponse = {
product: productResponse.data,
reviews: reviewsResponse.data,
inventory: inventoryResponse.data,
inStock: inventoryResponse.data.quantity > 0
};
res.json(aggregatedResponse);
} catch (error) {
console.error('Error aggregating product details:', error);
res.status(500).json({
error: 'Failed to retrieve product details',
message: error.message
});
}
});
app.listen(8000, () => {
console.log('API Gateway running on port 8000');
});
Benefits of Request Aggregation
- Reduced network overhead: Clients make a single request instead of multiple
- Simplified client code: Complexity of coordinating multiple services is handled by the gateway
- Better mobile experience: Particularly valuable for mobile clients with limited bandwidth or high-latency connections
- Backend evolution flexibility: Services can be restructured without changing client code
Protocol Translation
Converting between different communication protocols used by clients and services.
// Example of protocol translation from REST to gRPC in Node.js
const express = require('express');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const app = express();
app.use(express.json());
// Load gRPC service definition
const packageDefinition = protoLoader.loadSync('product.proto');
const productProto = grpc.loadPackageDefinition(packageDefinition).product;
// Create gRPC client
const productClient = new productProto.ProductService(
'product-service:50051',
grpc.credentials.createInsecure()
);
// REST endpoint that translates to gRPC
app.get('/products/:id', (req, res) => {
const productId = req.params.id;
// Call gRPC service
productClient.getProduct({ productId }, (err, response) => {
if (err) {
console.error('Error calling product service:', err);
return res.status(500).json({ error: 'Failed to retrieve product' });
}
// Transform response if needed
const product = {
id: response.id,
name: response.name,
description: response.description,
price: response.price,
imageUrl: response.imageUrl,
// Add any transformations needed
};
res.json(product);
});
});
app.listen(8000, () => {
console.log('API Gateway running on port 8000');
});
Authentication and Authorization
Handling security concerns at the gateway level, simplifying backend services.
// Example of JWT authentication in an Express gateway
const express = require('express');
const jwt = require('jsonwebtoken');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// Authentication middleware
const authenticate = (req, res, next) => {
// Get token from Authorization header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
// Verify the token
const decoded = jwt.verify(token, JWT_SECRET);
// Add user info to request for downstream services
req.user = decoded;
// Add user info as headers for services
req.headers['X-User-Id'] = decoded.userId;
req.headers['X-User-Role'] = decoded.role;
next();
} catch (error) {
console.error('Token verification failed:', error);
return res.status(401).json({ error: 'Invalid token' });
}
};
// Authorization middleware
const authorize = (requiredRoles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'User not authenticated' });
}
const userRole = req.user.role;
if (requiredRoles.includes(userRole)) {
next();
} else {
res.status(403).json({ error: 'Insufficient permissions' });
}
};
};
// Public routes
app.use('/api/auth', createProxyMiddleware({
target: 'http://auth-service:3001',
changeOrigin: true
}));
// Protected routes with authentication
app.use('/api/users', authenticate, createProxyMiddleware({
target: 'http://user-service:3002',
changeOrigin: true
}));
// Routes requiring specific roles
app.use('/api/admin',
authenticate,
authorize(['ADMIN']),
createProxyMiddleware({
target: 'http://admin-service:3003',
changeOrigin: true
})
);
app.listen(8000, () => {
console.log('API Gateway running on port 8000');
});
Security Responsibilities of API Gateways
- Authentication: Validating user identity via tokens, API keys, or certificates
- Authorization: Checking permissions for specific resources
- Input validation: Preventing injection attacks
- Rate limiting: Protecting against DoS attacks
- IP filtering: Blocking suspicious IP addresses
- TLS termination: Handling HTTPS encryption/decryption
Gateway Patterns and Architectures
There are several architectural patterns for implementing API Gateways in microservices:
Single Gateway Pattern
One gateway handling all requests for the entire system.
Single Gateway Pros and Cons
| Advantages | Disadvantages |
|---|---|
| Simplified infrastructure | Single point of failure |
| Consistent policy enforcement | Potential performance bottleneck |
| Easier monitoring | Cross-team coordination challenges |
| Simplified client configuration | May become bloated over time |
Gateway per Service Pattern
Each microservice gets its own dedicated gateway.
When to Use Gateway per Service
- When services have very different non-functional requirements
- For independent teams that want to control their entire stack
- When services need specialized security or access policies
- To enable independent scaling and deployment of gateways
Backend for Frontend (BFF) Pattern
Separate gateway for each type of client, optimized for their specific needs.
BFF Pattern Real-World Example: Netflix
Netflix implements the BFF pattern to serve different client platforms:
- TV BFF: Optimized for large screens, focuses on browsing and playback, minimal keyboard input
- Mobile BFF: Optimized for touch, data efficiency, battery usage, offline capabilities
- Web BFF: Richer interactions, account management, social features
- Partner BFF: Limited functionality, strict rate limiting, focused on content discovery
Each BFF exposes exactly what its client needs, in the format that works best for that client, while sharing the same backend services.
API Gateway Implementation Options
There are several approaches to implementing an API Gateway, from building one from scratch to using existing solutions:
Ready-to-Use API Gateway Products
Commercial and open-source solutions that provide gateway functionality out-of-the-box.
| Product | Type | Key Features | Best For |
|---|---|---|---|
| Kong | Open Source / Commercial | Plugin system, Kubernetes native, observability | Cloud-native, high-performance needs |
| Amazon API Gateway | Cloud Service | Lambda integration, serverless, pay-per-use | AWS-based microservices |
| Azure API Management | Cloud Service | Developer portal, policy framework | Azure-based systems, API marketplaces |
| Apigee | Commercial / Cloud | Advanced analytics, monetization, developer portal | Enterprise API programs, API monetization |
| Tyk | Open Source / Commercial | On-premises or cloud, GraphQL support | Self-hosted environments |
| NGINX + OpenResty | Open Source | High performance, Lua scripting | Performance-critical scenarios |
Framework-Based Implementation
Using web frameworks to build custom gateways tailored to your needs.
// Example of a simple API Gateway using Express.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const morgan = require('morgan');
const jwt = require('jsonwebtoken');
const app = express();
// Basic middleware
app.use(cors());
app.use(express.json());
app.use(morgan('combined'));
// Rate limiting
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later'
});
app.use(apiLimiter);
// Service discovery (simplified example)
const serviceRegistry = {
userService: 'http://user-service:3001',
productService: 'http://product-service:3002',
orderService: 'http://order-service:3003',
paymentService: 'http://payment-service:3004'
};
// Authentication middleware
const authenticate = (req, res, next) => {
// Implementation as shown earlier
};
// Circuit breaker middleware (simplified)
const circuitBreaker = (serviceName) => {
let failureCount = 0;
let isOpen = false;
let lastFailureTime = null;
return (req, res, next) => {
if (isOpen) {
const now = Date.now();
const timeElapsed = now - lastFailureTime;
if (timeElapsed > 10000) { // 10 seconds reset
isOpen = false;
failureCount = 0;
} else {
return res.status(503).json({
error: 'Service temporarily unavailable',
service: serviceName
});
}
}
// Store the original end method
const originalEnd = res.end;
// Override the end method
res.end = function(chunk, encoding) {
// Check for successful response
if (res.statusCode >= 200 && res.statusCode < 300) {
failureCount = 0;
} else if (res.statusCode >= 500) {
failureCount++;
if (failureCount >= 5) { // Open circuit after 5 consecutive failures
isOpen = true;
lastFailureTime = Date.now();
}
}
// Call the original end method
return originalEnd.call(this, chunk, encoding);
};
next();
};
};
// Request aggregation endpoint
app.get('/api/product-details/:id', async (req, res) => {
// Implementation as shown earlier
});
// Proxy routes to microservices
app.use('/api/users',
authenticate,
circuitBreaker('userService'),
createProxyMiddleware({
target: serviceRegistry.userService,
changeOrigin: true,
pathRewrite: {'^/api/users': ''}
})
);
app.use('/api/products',
circuitBreaker('productService'),
createProxyMiddleware({
target: serviceRegistry.productService,
changeOrigin: true,
pathRewrite: {'^/api/products': ''}
})
);
app.use('/api/orders',
authenticate,
circuitBreaker('orderService'),
createProxyMiddleware({
target: serviceRegistry.orderService,
changeOrigin: true,
pathRewrite: {'^/api/orders': ''}
})
);
app.use('/api/payments',
authenticate,
circuitBreaker('paymentService'),
createProxyMiddleware({
target: serviceRegistry.paymentService,
changeOrigin: true,
pathRewrite: {'^/api/payments': ''}
})
);
// Error handling
app.use((err, req, res, next) => {
console.error('Gateway Error:', err);
res.status(500).json({ error: 'Internal Server Error', message: err.message });
});
app.listen(8000, () => {
console.log('API Gateway running on port 8000');
});
Popular Frameworks for Building Gateways
- Express Gateway: Node.js gateway built on Express
- Spring Cloud Gateway: JVM-based reactive API gateway
- Traefik: Modern HTTP reverse proxy and load balancer
- Ocelot: .NET Core API Gateway
- Zuul/Zuul 2: JVM gateway by Netflix
- FastAPI: Python framework suitable for API gateways
Serverless API Gateways
Using serverless technologies to build scalable, pay-per-use gateways.
AWS Serverless API Gateway Example
Using AWS API Gateway + Lambda for a serverless gateway architecture:
// AWS SAM template for a serverless API Gateway
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
MyApiGateway:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Cors:
AllowMethods: "'GET,POST,PUT,DELETE'"
AllowHeaders: "'Content-Type,Authorization'"
AllowOrigin: "'*'"
Auth:
DefaultAuthorizer: JwtAuthorizer
Authorizers:
JwtAuthorizer:
FunctionArn: !GetAtt AuthorizerFunction.Arn
AuthorizerFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./authorizer/
Handler: index.handler
Runtime: nodejs14.x
Environment:
Variables:
JWT_SECRET: !Ref JwtSecret
ProductAggregatorFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./product-aggregator/
Handler: index.handler
Runtime: nodejs14.x
Events:
ApiEvent:
Type: Api
Properties:
RestApiId: !Ref MyApiGateway
Path: /products/{id}
Method: get
Environment:
Variables:
PRODUCT_SERVICE_URL: !Ref ProductServiceUrl
REVIEW_SERVICE_URL: !Ref ReviewServiceUrl
INVENTORY_SERVICE_URL: !Ref InventoryServiceUrl
UserProxyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./user-proxy/
Handler: index.handler
Runtime: nodejs14.x
Events:
ApiEvent:
Type: Api
Properties:
RestApiId: !Ref MyApiGateway
Path: /users/{proxy+}
Method: any
Environment:
Variables:
USER_SERVICE_URL: !Ref UserServiceUrl
Parameters:
JwtSecret:
Type: String
NoEcho: true
Description: Secret for JWT verification
ProductServiceUrl:
Type: String
Description: URL for the Product Service
ReviewServiceUrl:
Type: String
Description: URL for the Review Service
InventoryServiceUrl:
Type: String
Description: URL for the Inventory Service
UserServiceUrl:
Type: String
Description: URL for the User Service
Outputs:
ApiEndpoint:
Description: "API Gateway endpoint URL"
Value: !Sub "https://${MyApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod/"
Advanced Gateway Features
Modern API Gateways implement several advanced features for building robust microservices architectures:
Service Discovery Integration
Dynamically finding and routing to available service instances.
// Example of Consul service discovery integration in Node.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const Consul = require('consul');
const app = express();
const consul = new Consul({
host: 'consul-server',
port: 8500
});
// Dynamic proxy middleware based on service discovery
const createServiceProxy = (serviceName) => {
return async (req, res, next) => {
try {
// Look up service in Consul
const serviceResult = await new Promise((resolve, reject) => {
consul.catalog.service.nodes(serviceName, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
if (!serviceResult || serviceResult.length === 0) {
return res.status(503).json({ error: `Service ${serviceName} not available` });
}
// Simple round-robin load balancing
const serviceInstance = serviceResult[Math.floor(Math.random() * serviceResult.length)];
const serviceUrl = `http://${serviceInstance.ServiceAddress}:${serviceInstance.ServicePort}`;
// Create proxy on the fly
const proxy = createProxyMiddleware({
target: serviceUrl,
changeOrigin: true,
onError: (err, req, res) => {
console.error(`Proxy error for ${serviceName}:`, err);
res.status(500).json({ error: `Error connecting to ${serviceName}` });
}
});
// Call the proxy middleware
return proxy(req, res, next);
} catch (error) {
console.error(`Service discovery error for ${serviceName}:`, error);
return res.status(500).json({ error: 'Service discovery failed' });
}
};
};
// Routes using dynamic service discovery
app.use('/api/users', createServiceProxy('user-service'));
app.use('/api/products', createServiceProxy('product-service'));
app.use('/api/orders', createServiceProxy('order-service'));
app.listen(8000, () => {
console.log('API Gateway running on port 8000');
});
Rate Limiting and Throttling
Controlling the rate of requests to protect backend services.
// Example of advanced rate limiting in Express
const express = require('express');
const redis = require('redis');
const { RateLimiterRedis } = require('rate-limiter-flexible');
const app = express();
const redisClient = redis.createClient({
host: 'redis-server',
port: 6379,
enable_offline_queue: false
});
// General rate limiter
const generalLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'general',
points: 100, // Maximum number of requests
duration: 60, // Per 60 seconds
});
// Endpoint-specific rate limiter (stricter)
const paymentLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'payment',
points: 20, // Maximum number of requests
duration: 60, // Per 60 seconds
});
// Rate limiting middleware
const rateLimiterMiddleware = (limiter) => {
return (req, res, next) => {
// Use IP or user ID as key
const key = req.user ? req.user.id : req.ip;
limiter.consume(key)
.then(() => {
next();
})
.catch((rejRes) => {
// Add rate limit headers
res.set('Retry-After', Math.round(rejRes.msBeforeNext / 1000));
res.set('X-RateLimit-Limit', limiter.points);
res.set('X-RateLimit-Remaining', rejRes.remainingPoints);
res.set('X-RateLimit-Reset', new Date(Date.now() + rejRes.msBeforeNext));
res.status(429).json({
error: 'Too Many Requests',
message: 'Rate limit exceeded',
retryAfter: Math.round(rejRes.msBeforeNext / 1000)
});
});
};
};
// Apply rate limiters
app.use(rateLimiterMiddleware(generalLimiter)); // Global rate limit
// Apply stricter limits to specific endpoints
app.use('/api/payments', rateLimiterMiddleware(paymentLimiter));
app.listen(8000, () => {
console.log('API Gateway running on port 8000');
});
Rate Limiting Strategies
- Fixed window: X requests per fixed time period (per hour, day, etc.)
- Sliding window: X requests over a rolling time period
- Token bucket: Tokens accumulate at a fixed rate, each request consumes a token
- Leaky bucket: Requests processed at a constant rate, excess are queued or dropped
- Adaptive limiting: Dynamically adjusts limits based on backend health
Response Caching
Storing responses to improve performance and reduce load on backend services.
// Example of response caching with Redis in Express
const express = require('express');
const redis = require('redis');
const { promisify } = require('util');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
const redisClient = redis.createClient({
host: 'redis-server',
port: 6379
});
const getAsync = promisify(redisClient.get).bind(redisClient);
const setexAsync = promisify(redisClient.setex).bind(redisClient);
// Caching middleware
const cacheMiddleware = (duration) => {
return async (req, res, next) => {
// Skip caching for non-GET requests
if (req.method !== 'GET') {
return next();
}
// Create a cache key based on the URL
const cacheKey = `cache:${req.originalUrl}`;
try {
// Try to get cached response
const cachedResponse = await getAsync(cacheKey);
if (cachedResponse) {
const parsedResponse = JSON.parse(cachedResponse);
// Set cache hit header
res.set('X-Cache', 'HIT');
// Return the cached response
return res.status(parsedResponse.status)
.set(parsedResponse.headers)
.send(parsedResponse.body);
}
// If not cached, intercept the response to cache it
const originalSend = res.send;
res.send = function(body) {
const response = {
status: res.statusCode,
headers: {
'Content-Type': res.get('Content-Type')
},
body: body
};
// Only cache successful responses
if (res.statusCode >= 200 && res.statusCode < 300) {
setexAsync(cacheKey, duration, JSON.stringify(response))
.catch(err => console.error('Cache storage error:', err));
}
// Set cache miss header
res.set('X-Cache', 'MISS');
// Call the original send
return originalSend.call(this, body);
};
next();
} catch (error) {
console.error('Cache error:', error);
next(); // Proceed without caching on error
}
};
};
// Apply caching to product listings (cache for 5 minutes)
app.use('/api/products',
cacheMiddleware(300),
createProxyMiddleware({
target: 'http://product-service:3000',
changeOrigin: true
})
);
// Apply shorter caching to product details (cache for 1 minute)
app.use('/api/products/:id',
cacheMiddleware(60),
createProxyMiddleware({
target: 'http://product-service:3000',
changeOrigin: true
})
);
// No caching for user-specific data
app.use('/api/users',
createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true
})
);
app.listen(8000, () => {
console.log('API Gateway running on port 8000');
});
Request/Response Transformation
Modifying requests and responses as they pass through the gateway.
// Example of request/response transformation in Express
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// Request transformer for backward compatibility
const transformLegacyRequest = (req, res, next) => {
// Transform v1 format to v2 format
if (req.query.user_id) {
req.query.userId = req.query.user_id;
delete req.query.user_id;
}
if (req.body) {
if (req.body.user_name) {
req.body.userName = req.body.user_name;
delete req.body.user_name;
}
if (req.body.items) {
req.body.orderItems = req.body.items.map(item => ({
productId: item.product_id,
quantity: item.qty,
unitPrice: item.price
}));
delete req.body.items;
}
}
next();
};
// Response transformer for backward compatibility
const transformNewResponse = (proxyRes, req, res) => {
const originalSend = res.send;
res.send = function(body) {
// Parse JSON body if it's a string
let parsedBody = typeof body === 'string' ? JSON.parse(body) : body;
// Transform v2 format to v1 format for legacy clients
if (req.headers['x-api-version'] === 'v1') {
if (parsedBody.userName) {
parsedBody.user_name = parsedBody.userName;
delete parsedBody.userName;
}
if (parsedBody.orderItems) {
parsedBody.items = parsedBody.orderItems.map(item => ({
product_id: item.productId,
qty: item.quantity,
price: item.unitPrice
}));
delete parsedBody.orderItems;
}
}
// Convert back to string if needed
const transformedBody = typeof body === 'string'
? JSON.stringify(parsedBody)
: parsedBody;
return originalSend.call(this, transformedBody);
};
};
// Apply transformations to order service
app.use('/api/orders',
transformLegacyRequest,
createProxyMiddleware({
target: 'http://order-service:3003',
changeOrigin: true,
onProxyRes: transformNewResponse
})
);
app.listen(8000, () => {
console.log('API Gateway running on port 8000');
});
Gateway Deployment Strategies
API Gateways can be deployed in various ways to meet different operational needs:
Containerized Deployment
Deploying the API Gateway as containers in an orchestration platform like Kubernetes.
# Example Kubernetes deployment for an API Gateway
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
namespace: microservices
spec:
replicas: 3
selector:
matchLabels:
app: api-gateway
template:
metadata:
labels:
app: api-gateway
spec:
containers:
- name: api-gateway
image: myregistry/api-gateway:1.0.0
ports:
- containerPort: 8000
resources:
limits:
cpu: "1"
memory: "1Gi"
requests:
cpu: "500m"
memory: "512Mi"
env:
- name: USER_SERVICE_URL
value: "http://user-service.microservices.svc.cluster.local:3001"
- name: PRODUCT_SERVICE_URL
value: "http://product-service.microservices.svc.cluster.local:3002"
- name: ORDER_SERVICE_URL
value: "http://order-service.microservices.svc.cluster.local:3003"
- name: REDIS_HOST
value: "redis.microservices.svc.cluster.local"
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: api-gateway-secrets
key: jwt-secret
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 15
periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: api-gateway
namespace: microservices
spec:
selector:
app: api-gateway
ports:
- port: 80
targetPort: 8000
type: LoadBalancer
---
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: api-gateway-hpa
namespace: microservices
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-gateway
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Multi-Region Deployment
Deploying gateways across multiple geographic regions for lower latency and higher availability.
Multi-Region Strategy Considerations
- Load balancing: DNS-based, anycast, or application-level load balancing
- Latency routing: Directing users to the nearest gateway
- Failover routing: Redirecting traffic if a region goes down
- Data synchronization: Managing cache and state across regions
- Configuration consistency: Ensuring all gateways have the same configuration
- Compliance: Handling regional data sovereignty requirements
Edge Deployment (CDN Integration)
Deploying API Gateways at the network edge for minimal latency.
Edge Gateway with Cloudflare Workers
Example of a simple API Gateway implemented as a Cloudflare Worker:
// Cloudflare Worker API Gateway
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
// Service registry
const SERVICES = {
'users': 'https://user-service.example.com',
'products': 'https://product-service.example.com',
'orders': 'https://order-service.example.com'
}
async function handleRequest(request) {
const url = new URL(request.url)
const path = url.pathname
// Basic health check
if (path === '/health') {
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' }
})
}
// Extract the service name from the path
const pathParts = path.split('/')
if (pathParts.length < 2) {
return new Response('Not Found', { status: 404 })
}
const service = pathParts[1]
// Check if service exists
if (!SERVICES[service]) {
return new Response('Service Not Found', { status: 404 })
}
// Authentication (simplified)
const auth = request.headers.get('Authorization')
if (!auth && service !== 'health') {
return new Response('Unauthorized', { status: 401 })
}
// Rate limiting using Cloudflare's built-in rate limiting
// Create a new request to the backend service
const serviceUrl = SERVICES[service]
const newPath = '/' + pathParts.slice(2).join('/')
const newUrl = serviceUrl + newPath + url.search
// Clone request and modify headers if needed
const newRequest = new Request(newUrl, {
method: request.method,
headers: request.headers,
body: request.body,
redirect: 'follow'
})
try {
// Forward request to backend service
const response = await fetch(newRequest)
// Clone the response and modify if needed
const newResponse = new Response(response.body, response)
// Add CORS headers
newResponse.headers.set('Access-Control-Allow-Origin', '*')
newResponse.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
newResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
return newResponse
} catch (error) {
return new Response(JSON.stringify({ error: 'Gateway Error', message: error.message }), {
status: 502,
headers: { 'Content-Type': 'application/json' }
})
}
}
Monitoring and Observability
API Gateways are critical infrastructure components that require comprehensive monitoring:
Metrics Collection
Key metrics to collect from an API Gateway:
- Request rate: Requests per second, by endpoint and client
- Latency: Response time percentiles (p50, p90, p99)
- Error rate: Percentage of requests resulting in errors
- Circuit breaker status: Open/closed state for each service
- Cache hit ratio: Percentage of requests served from cache
- Rate limiting: Number of throttled requests
// Example of metrics collection with Prometheus in Express
const express = require('express');
const promClient = require('prom-client');
const promBundle = require('express-prom-bundle');
const app = express();
// Create custom metrics
const requestLatency = new promClient.Histogram({
name: 'gateway_request_duration_seconds',
help: 'Request duration in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10]
});
const circuitBreakerStatus = new promClient.Gauge({
name: 'gateway_circuit_breaker_status',
help: 'Circuit breaker status (1=closed, 0=open)',
labelNames: ['service']
});
const cacheHitRatio = new promClient.Gauge({
name: 'gateway_cache_hit_ratio',
help: 'Cache hit ratio',
labelNames: ['route']
});
const rateLimitedRequests = new promClient.Counter({
name: 'gateway_rate_limited_requests_total',
help: 'Total number of rate limited requests',
labelNames: ['client_id', 'route']
});
// Initialize metrics middleware
const metricsMiddleware = promBundle({
includeMethod: true,
includePath: true,
includeStatusCode: true,
includeUp: true,
promClient: {
collectDefaultMetrics: {}
}
});
// Apply metrics middleware
app.use(metricsMiddleware);
// Circuit breaker middleware with metrics
const circuitBreaker = (serviceName) => {
let failureCount = 0;
let isOpen = false;
let lastFailureTime = null;
// Initialize circuit breaker as closed
circuitBreakerStatus.set({ service: serviceName }, 1);
return (req, res, next) => {
if (isOpen) {
// Circuit is open, update metric
circuitBreakerStatus.set({ service: serviceName }, 0);
const now = Date.now();
const timeElapsed = now - lastFailureTime;
if (timeElapsed > 10000) { // 10 seconds reset
isOpen = false;
failureCount = 0;
// Circuit is closed again, update metric
circuitBreakerStatus.set({ service: serviceName }, 1);
} else {
return res.status(503).json({
error: 'Service temporarily unavailable',
service: serviceName
});
}
}
// Request timing
const start = Date.now();
// Store the original end method
const originalEnd = res.end;
// Override the end method
res.end = function(chunk, encoding) {
// Calculate request duration
const duration = (Date.now() - start) / 1000;
// Record request latency
requestLatency.observe({
method: req.method,
route: req.route ? req.route.path : req.path,
status_code: res.statusCode
}, duration);
// Check for successful response
if (res.statusCode >= 200 && res.statusCode < 300) {
failureCount = 0;
} else if (res.statusCode >= 500) {
failureCount++;
if (failureCount >= 5) { // Open circuit after 5 consecutive failures
isOpen = true;
lastFailureTime = Date.now();
// Circuit is open, update metric
circuitBreakerStatus.set({ service: serviceName }, 0);
}
}
// Call the original end method
return originalEnd.call(this, chunk, encoding);
};
next();
};
};
// Example of cache metrics middleware
const cacheMetricsMiddleware = (route) => {
let hits = 0;
let misses = 0;
// Update hit ratio every minute
setInterval(() => {
const total = hits + misses;
if (total > 0) {
cacheHitRatio.set({ route }, hits / total);
}
}, 60000);
return (req, res, next) => {
// Track cache hits/misses
res.on('finish', () => {
const cacheHeader = res.getHeader('X-Cache');
if (cacheHeader === 'HIT') {
hits++;
} else if (cacheHeader === 'MISS') {
misses++;
}
});
next();
};
};
// Apply route-specific middleware
app.use('/api/products',
cacheMetricsMiddleware('/api/products'),
createProxyMiddleware({
target: 'http://product-service:3000',
changeOrigin: true
})
);
// Metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
app.listen(8000, () => {
console.log('API Gateway running on port 8000');
});
Distributed Tracing
Tracking requests as they flow through multiple services.
// Example of distributed tracing integration with OpenTelemetry in Express
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
// OpenTelemetry setup
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const opentelemetry = require('@opentelemetry/api');
// Configure the tracer
const provider = new NodeTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'api-gateway',
}),
});
// Configure the exporter
const exporter = new JaegerExporter({
serviceName: 'api-gateway',
endpoint: 'http://jaeger:14268/api/traces',
});
// Add the exporter to the provider
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();
// Register instrumentations
registerInstrumentations({
instrumentations: [
new ExpressInstrumentation(),
new HttpInstrumentation({
requestHook: (span, request) => {
span.setAttribute('http.request.headers', JSON.stringify(request.headers));
},
}),
],
});
// Get the tracer
const tracer = opentelemetry.trace.getTracer('api-gateway');
const app = express();
// Manual tracing middleware for specific operations
const tracingMiddleware = (operationName) => {
return (req, res, next) => {
// Get the current span from context
const currentSpan = opentelemetry.trace.getSpan(opentelemetry.context.active());
// Create a custom span for the operation
tracer.startActiveSpan(operationName, span => {
// Add custom attributes
span.setAttribute('operation.name', operationName);
span.setAttribute('request.path', req.path);
span.setAttribute('request.method', req.method);
// Store the span in the request for later use
req.operationSpan = span;
// Add finish handler
res.on('finish', () => {
// Add response info to span
span.setAttribute('response.status_code', res.statusCode);
// Handle errors
if (res.statusCode >= 400) {
span.setStatus({
code: opentelemetry.SpanStatusCode.ERROR,
message: `Error: ${res.statusCode}`
});
}
// End the span
span.end();
});
next();
});
};
};
// Apply tracing to proxy routes
app.use('/api/users',
tracingMiddleware('proxy_users_service'),
createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true,
onProxyReq: (proxyReq, req, res) => {
// Add the current trace context to the outgoing request
const currentContext = opentelemetry.context.active();
const traceparent = opentelemetry.propagation.inject(currentContext, {});
// Copy trace headers to the proxied request
Object.keys(traceparent).forEach(key => {
proxyReq.setHeader(key, traceparent[key]);
});
}
})
);
app.listen(8000, () => {
console.log('API Gateway running on port 8000');
});
Logging and Auditing
Comprehensive logging for debugging and compliance:
// Example of structured logging in an API Gateway
const express = require('express');
const winston = require('winston');
const { createProxyMiddleware } = require('http-proxy-middleware');
const { v4: uuidv4 } = require('uuid');
// Configure logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'api-gateway' },
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'gateway-error.log', level: 'error' }),
new winston.transports.File({ filename: 'gateway.log' })
]
});
const app = express();
// Request ID middleware
app.use((req, res, next) => {
// Generate or use existing request ID
req.id = req.headers['x-request-id'] || uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
});
// Logging middleware
app.use((req, res, next) => {
const start = Date.now();
// Log request
logger.info({
message: 'Request received',
requestId: req.id,
method: req.method,
url: req.originalUrl,
ip: req.ip,
userAgent: req.headers['user-agent'],
userId: req.user?.id
});
// Log response
res.on('finish', () => {
const duration = Date.now() - start;
const logLevel = res.statusCode >= 400 ? 'error' : 'info';
logger[logLevel]({
message: 'Response sent',
requestId: req.id,
method: req.method,
url: req.originalUrl,
statusCode: res.statusCode,
duration,
contentLength: res.get('Content-Length'),
userId: req.user?.id
});
});
next();
});
// Audit logging for sensitive operations
const auditLog = (operation, details) => {
logger.info({
message: 'AUDIT',
operation,
...details,
timestamp: new Date().toISOString()
});
};
// Sensitive operations middleware
app.use('/api/payments', (req, res, next) => {
// Capture original data for audit
const originalBody = JSON.parse(JSON.stringify(req.body || {}));
// Mask sensitive data for logging
const maskedBody = { ...originalBody };
if (maskedBody.creditCard) {
maskedBody.creditCard = `****-****-****-${maskedBody.creditCard.slice(-4)}`;
}
// Log the sensitive operation
auditLog('payment_operation', {
requestId: req.id,
userId: req.user?.id,
method: req.method,
path: req.path,
data: maskedBody
});
next();
});
// Apply proxy middleware
app.use('/api/users', createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true,
logProvider: () => logger
}));
// Error handling
app.use((err, req, res, next) => {
logger.error({
message: 'Gateway error',
requestId: req.id,
error: {
name: err.name,
message: err.message,
stack: err.stack
},
method: req.method,
url: req.originalUrl
});
res.status(500).json({ error: 'Internal Server Error' });
});
app.listen(8000, () => {
logger.info('API Gateway running on port 8000');
});
Practical Exercise
Building a Simple API Gateway for Microservices
In this exercise, you'll implement a basic API Gateway for a microservices architecture with the following components:
- User Service (authentication, user profiles)
- Product Service (product catalog, inventory)
- Order Service (order processing, history)
Your gateway should implement the following features:
- Request routing to the appropriate service
- JWT authentication and authorization
- Request logging and basic metrics collection
- Basic request aggregation for a product details endpoint
- Simple cache implementation for product listings
You can use Express.js, Spring Boot, or any other framework of your choice.
Starter Code for Express.js Implementation
const express = require('express');
const jwt = require('jsonwebtoken');
const { createProxyMiddleware } = require('http-proxy-middleware');
const axios = require('axios');
const NodeCache = require('node-cache');
const app = express();
app.use(express.json());
// Simple in-memory cache
const cache = new NodeCache({ stdTTL: 300 }); // 5-minute TTL
// Service URLs (would normally come from service discovery)
const SERVICES = {
user: 'http://localhost:3001',
product: 'http://localhost:3002',
order: 'http://localhost:3003'
};
// JWT Secret (would normally be in environment variable)
const JWT_SECRET = 'your-secret-key';
// Add your middleware and routes here...
app.listen(8000, () => {
console.log('API Gateway running on port 8000
})
Complete the starter code by adding the following components:
- Authentication middleware
- Request routing logic
- Caching implementation
- Logging middleware
- Product details aggregation endpoint
Conclusion and Key Takeaways
- API Gateways serve as a crucial entry point to microservices architectures, handling routing, aggregation, and cross-cutting concerns
- Key gateway responsibilities include request routing, aggregation, protocol translation, and security enforcement
- Several architectural patterns exist, including single gateway, gateway per service, and Backend for Frontend (BFF)
- Gateways can be implemented using off-the-shelf products, custom frameworks, or cloud-based services
- Advanced features like service discovery, rate limiting, caching, and request transformation improve gateway functionality
- Proper monitoring and observability are essential for gateway reliability and troubleshooting
- Deploying gateways requires careful consideration of scalability, availability, and geographic distribution
When implemented correctly, API Gateways simplify client integration, improve security, and enable independent evolution of backend services while providing a consistent interface to clients.