Back to Blog
API GatewayMicroservicesArchitectureRouting

API Gateway Patterns for Microservices

Design effective API gateways that handle routing, authentication, rate limiting, and more. From basic proxying to advanced patterns.

B
Bootspring Team
Engineering
February 2, 2025
6 min read

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: false

Monitoring#

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.

Share this article

Help spread the word about Bootspring