An API Gateway is the single entry point for all client requests in a microservices architecture. It handles cross-cutting concerns like authentication, rate limiting, and request routing.
Why API Gateways?#
Without Gateway:
┌────────┐ ┌─────────────┐
│ Client │────▶│ Service A │
└────────┘ └─────────────┘
│ ┌─────────────┐
└─────────▶│ Service B │
│ └─────────────┘
└─────────▶│ Service C │
└─────────────┘
Problems:
- Client knows all services
- No central auth/rate limiting
- Complex client logic
With Gateway:
┌────────┐ ┌─────────┐ ┌─────────────┐
│ Client │────▶│ Gateway │────▶│ Service A │
└────────┘ └─────────┘ ├─────────────┤
│ │ Service B │
│ ├─────────────┤
└─────────▶│ Service C │
└─────────────┘
Core Functions#
Request Routing#
1// Route based on path
2const routes: Route[] = [
3 { path: '/users/*', service: 'user-service' },
4 { path: '/orders/*', service: 'order-service' },
5 { path: '/products/*', service: 'product-service' },
6 { path: '/payments/*', service: 'payment-service' },
7];
8
9async function routeRequest(req: Request): Promise<Response> {
10 const route = routes.find(r => req.path.startsWith(r.path.replace('*', '')));
11
12 if (!route) {
13 return new Response('Not Found', { status: 404 });
14 }
15
16 const serviceUrl = await serviceRegistry.getUrl(route.service);
17 return fetch(`${serviceUrl}${req.path}`, {
18 method: req.method,
19 headers: req.headers,
20 body: req.body,
21 });
22}Authentication#
1async function authenticate(req: Request): Promise<User | null> {
2 const token = req.headers.get('Authorization')?.replace('Bearer ', '');
3
4 if (!token) return null;
5
6 try {
7 const user = jwt.verify(token, process.env.JWT_SECRET);
8 return user as User;
9 } catch {
10 return null;
11 }
12}
13
14async function gatewayMiddleware(req: Request): Promise<Response> {
15 // Public routes
16 if (isPublicRoute(req.path)) {
17 return routeRequest(req);
18 }
19
20 // Authenticate
21 const user = await authenticate(req);
22 if (!user) {
23 return new Response('Unauthorized', { status: 401 });
24 }
25
26 // Add user to headers for downstream services
27 req.headers.set('X-User-Id', user.id);
28 req.headers.set('X-User-Role', user.role);
29
30 return routeRequest(req);
31}Rate Limiting#
1const rateLimiter = new RateLimiter({
2 windowMs: 60000, // 1 minute
3 max: 100, // 100 requests per window
4});
5
6async function rateLimitMiddleware(
7 req: Request,
8 ctx: Context,
9): Promise<Response | null> {
10 const key = ctx.user?.id || req.ip;
11 const result = await rateLimiter.check(key);
12
13 if (!result.allowed) {
14 return new Response('Too Many Requests', {
15 status: 429,
16 headers: {
17 'Retry-After': String(result.retryAfter),
18 'X-RateLimit-Limit': String(result.limit),
19 'X-RateLimit-Remaining': '0',
20 'X-RateLimit-Reset': String(result.resetAt),
21 },
22 });
23 }
24
25 return null; // Continue to next middleware
26}Advanced Patterns#
Request Aggregation (BFF Pattern)#
1// Backend for Frontend - aggregate multiple services
2async function getProductPage(productId: string): Promise<ProductPage> {
3 // Parallel requests to multiple services
4 const [product, reviews, recommendations, inventory] = await Promise.all([
5 productService.getProduct(productId),
6 reviewService.getReviews(productId),
7 recommendationService.getRecommendations(productId),
8 inventoryService.getStock(productId),
9 ]);
10
11 return {
12 product,
13 reviews: reviews.slice(0, 5),
14 recommendations: recommendations.slice(0, 4),
15 inStock: inventory.available > 0,
16 stockLevel: inventory.available,
17 };
18}
19
20// Endpoint
21app.get('/api/bff/product/:id', async (req, res) => {
22 const page = await getProductPage(req.params.id);
23 res.json(page);
24});Response Transformation#
1// Transform legacy API responses
2async function transformResponse(
3 response: Response,
4 route: Route,
5): Promise<Response> {
6 if (route.transform) {
7 const body = await response.json();
8 const transformed = route.transform(body);
9 return new Response(JSON.stringify(transformed), {
10 status: response.status,
11 headers: response.headers,
12 });
13 }
14 return response;
15}
16
17const routes = [
18 {
19 path: '/api/v2/users/*',
20 service: 'legacy-user-service',
21 transform: (data: LegacyUser) => ({
22 id: data.user_id,
23 email: data.email_address,
24 name: `${data.first_name} ${data.last_name}`,
25 createdAt: data.created_date,
26 }),
27 },
28];Circuit Breaker#
1class CircuitBreaker {
2 private failures = 0;
3 private lastFailure: Date | null = null;
4 private state: 'closed' | 'open' | 'half-open' = 'closed';
5
6 constructor(
7 private threshold: number = 5,
8 private timeout: number = 60000,
9 ) {}
10
11 async execute<T>(fn: () => Promise<T>): Promise<T> {
12 if (this.state === 'open') {
13 if (Date.now() - this.lastFailure!.getTime() > this.timeout) {
14 this.state = 'half-open';
15 } else {
16 throw new Error('Circuit breaker is open');
17 }
18 }
19
20 try {
21 const result = await fn();
22 this.onSuccess();
23 return result;
24 } catch (error) {
25 this.onFailure();
26 throw error;
27 }
28 }
29
30 private onSuccess(): void {
31 this.failures = 0;
32 this.state = 'closed';
33 }
34
35 private onFailure(): void {
36 this.failures++;
37 this.lastFailure = new Date();
38 if (this.failures >= this.threshold) {
39 this.state = 'open';
40 }
41 }
42}
43
44// Usage
45const breakers = new Map<string, CircuitBreaker>();
46
47async function callService(service: string, request: Request): Promise<Response> {
48 let breaker = breakers.get(service);
49 if (!breaker) {
50 breaker = new CircuitBreaker();
51 breakers.set(service, breaker);
52 }
53
54 return breaker.execute(() => fetch(service, request));
55}Request/Response Caching#
1class GatewayCache {
2 private cache: Map<string, CacheEntry> = new Map();
3
4 getCacheKey(req: Request): string {
5 return `${req.method}:${req.url}`;
6 }
7
8 async get(req: Request): Promise<Response | null> {
9 if (req.method !== 'GET') return null;
10
11 const key = this.getCacheKey(req);
12 const entry = this.cache.get(key);
13
14 if (!entry) return null;
15 if (Date.now() > entry.expiresAt) {
16 this.cache.delete(key);
17 return null;
18 }
19
20 return new Response(entry.body, {
21 status: entry.status,
22 headers: { ...entry.headers, 'X-Cache': 'HIT' },
23 });
24 }
25
26 async set(req: Request, response: Response, ttl: number): Promise<void> {
27 if (req.method !== 'GET') return;
28 if (response.status !== 200) return;
29
30 const key = this.getCacheKey(req);
31 const body = await response.clone().text();
32
33 this.cache.set(key, {
34 body,
35 status: response.status,
36 headers: Object.fromEntries(response.headers),
37 expiresAt: Date.now() + ttl,
38 });
39 }
40}Service Discovery#
1interface ServiceRegistry {
2 register(service: string, url: string): Promise<void>;
3 deregister(service: string, url: string): Promise<void>;
4 getInstances(service: string): Promise<string[]>;
5}
6
7// Consul-based discovery
8class ConsulRegistry implements ServiceRegistry {
9 async getInstances(service: string): Promise<string[]> {
10 const response = await fetch(
11 `${this.consulUrl}/v1/health/service/${service}?passing=true`
12 );
13 const services = await response.json();
14 return services.map((s: any) =>
15 `http://${s.Service.Address}:${s.Service.Port}`
16 );
17 }
18}
19
20// Load balancing
21class LoadBalancer {
22 private index = 0;
23
24 async getUrl(service: string): Promise<string> {
25 const instances = await registry.getInstances(service);
26 if (!instances.length) {
27 throw new Error(`No instances available for ${service}`);
28 }
29
30 // Round-robin
31 const url = instances[this.index % instances.length];
32 this.index++;
33 return url;
34 }
35}Implementation Options#
Express Gateway#
1import express from 'express';
2import { createProxyMiddleware } from 'http-proxy-middleware';
3
4const app = express();
5
6// Auth middleware
7app.use(authenticate);
8
9// Rate limiting
10app.use(rateLimit);
11
12// Service routing
13app.use('/api/users', createProxyMiddleware({
14 target: 'http://user-service:3001',
15 changeOrigin: true,
16 pathRewrite: { '^/api/users': '' },
17}));
18
19app.use('/api/orders', createProxyMiddleware({
20 target: 'http://order-service:3002',
21 changeOrigin: true,
22 pathRewrite: { '^/api/orders': '' },
23}));
24
25app.listen(3000);Kong/NGINX#
1# Kong declarative config
2services:
3 - name: user-service
4 url: http://user-service:3001
5 routes:
6 - name: user-route
7 paths:
8 - /api/users
9
10 - name: order-service
11 url: http://order-service:3002
12 routes:
13 - name: order-route
14 paths:
15 - /api/orders
16
17plugins:
18 - name: rate-limiting
19 config:
20 minute: 100
21
22 - name: jwt
23 config:
24 secret_is_base64: falseMonitoring#
1// Request logging
2app.use((req, res, next) => {
3 const start = Date.now();
4
5 res.on('finish', () => {
6 const duration = Date.now() - start;
7
8 logger.info({
9 method: req.method,
10 path: req.path,
11 status: res.statusCode,
12 duration,
13 userId: req.userId,
14 service: req.targetService,
15 });
16
17 metrics.histogram('gateway_request_duration', duration, {
18 method: req.method,
19 path: req.path,
20 status: res.statusCode,
21 });
22 });
23
24 next();
25});Conclusion#
API Gateways simplify microservices architectures by centralizing cross-cutting concerns. Start with basic routing and authentication, then add caching, circuit breakers, and aggregation as needed.
Choose your implementation based on scale: Express with http-proxy-middleware for simple cases, Kong or AWS API Gateway for production systems.