REST API Development

API Gateway Patterns

15 min Lesson 26 of 35

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:
  1. Three backend services: User Service (port 3000), Product Service (port 3001), Order Service (port 3002)
  2. Implement JWT authentication for all routes
  3. Apply rate limiting: 100 requests per 15 minutes for general endpoints, 10 requests per 15 minutes for authentication endpoints
  4. Create an aggregated endpoint /api/checkout/:orderId that fetches order details, user information, and product information in a single request
  5. Implement response caching for product listings (5 minutes TTL)
  6. Add health check endpoints for each backend service

Choose either Nginx, Kong, or Express.js and provide the complete configuration.