An API Gateway is the front door for your microservices, handling cross-cutting concerns and providing a unified interface to clients. Here's how to implement one effectively.
What Is an API Gateway?
An API Gateway sits between clients and backend services, handling routing, composition, and cross-cutting concerns like auth, rate limiting, and caching.
┌─────────┐
│ Client │
└────┬────┘
│
▼
┌─────────────┐
│ API Gateway │ ← Single entry point
└──────┬──────┘
│
├──────────┬──────────┬──────────┐
▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│User │ │Order │ │Payment│ │Product│
│Service│ │Service│ │Service│ │Service│
└──────┘ └──────┘ └──────┘ └──────┘Core Responsibilities
Request Routing
// Express-based API Gateway
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// Route to user service
app.use('/api/users', createProxyMiddleware({
target: 'http://users-service:3001',
changeOrigin: true,
}));
// Route to order service
app.use('/api/orders', createProxyMiddleware({
target: 'http://orders-service:3002',
changeOrigin: true,
}));
// Route to product service
app.use('/api/products', createProxyMiddleware({
target: 'http://products-service:3003',
changeOrigin: true,
}));
app.listen(3000);Authentication and Authorization
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
}
// Apply to protected routes
app.use('/api/*', authenticateToken);Rate Limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP',
});
// Apply to all requests
app.use(limiter);
// Stricter limits for expensive endpoints
const strictLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Rate limit exceeded for this endpoint',
});
app.post('/api/orders', strictLimiter);API Composition
BFF Pattern (Backend for Frontend)
Different gateways for different clients.
// Gateway for Web
app.get '/api/web/products/:id', async (req, res) => {
const product = await fetchProduct(req.params.id);
const reviews = await fetchReviews(req.params.id);
const recommendations = await fetchRecommendations(req.params.id);
res.json({
product,
reviews,
recommendations,
});
}
// Gateway for Mobile (lighter response)
app.get('/api/mobile/products/:id', async (req, res) => {
const product = await fetchProduct(req.params.id);
const reviews = await fetchReviews(req.params.id, { limit: 5 });
res.json({
product,
reviews,
});
}Aggregation
Combine multiple service responses.
app.get('/api/dashboard', async (req, res) => {
try {
const [stats, orders, users] = await Promise.all([
fetch('http://stats-service/stats').then(r => r.json()),
fetch('http://orders-service/orders').then(r => r.json()),
fetch('http://users-service/users').then(r => r.json()),
]);
res.json({
stats: stats.total,
recentOrders: orders.slice(0, 10),
activeUsers: users.filter(u => u.active).length,
});
} catch (error) {
res.status(500).json({ error: 'Service unavailable' });
}
});Caching Strategies
Response Caching
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes
app.get('/api/products/:id', async (req, res) => {
const cacheKey = `product:${req.params.id}`;
// Check cache first
const cached = cache.get(cacheKey);
if (cached) {
console.log('Cache hit');
return res.json(cached);
}
// Fetch from service
const product = await fetch(`http://products-service/products/${req.params.id}`)
.then(r => r.json());
// Store in cache
cache.set(cacheKey, product);
res.json(product);
});Cache Invalidation
// Invalidate on updates
app.put('/api/products/:id', async (req, res) => {
const updated = await fetch(`http://products-service/products/${req.params.id}`, {
method: 'PUT',
body: JSON.stringify(req.body),
}).then(r => r.json());
// Invalidate cache
cache.del(`product:${req.params.id}`);
res.json(updated);
});Load Balancing
Round-Robin
const services = [
'http://service1:3000',
'http://service2:3000',
'http://service3:3000',
];
let currentIndex = 0;
function getNextService() {
const service = services[currentIndex];
currentIndex = (currentIndex + 1) % services.length;
return service;
}
app.use('/api/users', (req, res) => {
const target = getNextService();
proxy.web(req, res, { target });
});Least Connections
const serviceConnections = new Map();
function getServiceWithLeastConnections() {
let minService = services[0];
let minConnections = Infinity;
for (const service of services) {
const connections = serviceConnections.get(service) || 0;
if (connections < minConnections) {
minConnections = connections;
minService = service;
}
}
return minService;
}
app.use('/api/users', (req, res) => {
const service = getServiceWithLeastConnections();
// Increment connections
serviceConnections.set(service, (serviceConnections.get(service) || 0) + 1);
proxy.web(req, res, { target }, () => {
// Decrement when done
serviceConnections.set(service, serviceConnections.get(service) - 1);
});
});Circuit Breaker
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
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.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
// Usage
const circuitBreaker = new CircuitBreaker();
app.get('/api/users', async (req, res) => {
try {
const users = await circuitBreaker.execute(async () => {
return fetch('http://users-service/users').then(r => r.json());
});
res.json(users);
} catch (error) {
res.status(503).json({ error: 'Service unavailable' });
}
});Request/Response Transformation
Request Transformation
function transformRequest(req, res, next) {
// Add headers
req.headers['X-Request-ID'] = generateId();
req.headers['X-Forwarded-For'] = req.ip;
// Transform body
if (req.body) {
req.body = {
...req.body,
timestamp: new Date().toISOString(),
gateway: 'api-gateway-v1',
};
}
next();
}
app.use('/api/*', transformRequest);Response Transformation
function transformResponse(req, res, next) {
const originalSend = res.send;
res.send = function(data) {
// Envelope response
const response = {
success: res.statusCode < 400,
data,
meta: {
requestId: req.headers['X-Request-ID'],
timestamp: new Date().toISOString(),
},
};
originalSend.call(this, response);
};
next();
}
app.use('/api/*', transformResponse);Monitoring and Logging
Request Logging
const morgan = require('morgan');
// Log requests
app.use(morgan('combined'));
// Custom logging
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`
${req.method} ${req.url}
Status: ${res.statusCode}
Duration: ${duration}ms
`);
});
next();
});Metrics Collection
const promClient = require('prom-client');
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
});
app.use((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.url,
status_code: res.statusCode,
},
duration
);
});
next();
});
// Metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});Popular API Gateway Solutions
Kong
# Kong configuration
services:
- name: user-service
url: http://users-service:3001
routes:
- name: user-routes
service: user-service
paths:
- /api/usersAWS API Gateway
// Serverless framework
provider:
name: aws
runtime: nodejs18.x
functions:
getUsers:
handler: handler.getUsers
events:
- http:
path: users
method: get
cors: trueTraefik
# traefik.yml
http:
routers:
user-service:
rule: "PathPrefix(`/api/users`)"
service: user-service
services:
user-service:
loadBalancer:
servers:
- url: "http://users-service:3001"Best Practices
DOs
- Use API Gateway for cross-cutting concerns
- Implement proper authentication and authorization
- Add rate limiting to prevent abuse
- Cache responses where appropriate
- Monitor and log all requests
- Implement circuit breakers for resilience
- Use HTTPS for all communication
DON'Ts
- Create a monolithic gateway
- Add business logic to the gateway
- Overly complicate routing rules
- Ignore performance implications
- Skip error handling
- Forget about versioning
- Make the gateway a bottleneck
Conclusion
API Gateway is essential for microservices architecture. It provides a unified interface, handles cross-cutting concerns, and enables independent service evolution. Start with basic routing and auth, then add features like caching, rate limiting, and monitoring as needed. The right balance depends on your specific requirements.