API Gateway Implementation

Module 26: Advanced Backend & API Development

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
graph TD A[Mobile Client] --> G[API Gateway] B[Web Client] --> G C[Third-party Client] --> G G --> D[User Service] G --> E[Product Service] G --> F[Order Service] G --> H[Payment Service] G --> I[Notification Service] style G fill:#f8cecc,stroke:#b85450 style A,B,C fill:#dae8fc,stroke:#6c8ebf style D,E,F,H,I fill:#d5e8d4,stroke:#82b366

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.

graph LR A[Web Client] -->|HTTP/JSON| B[API Gateway] C[Mobile Client] -->|GraphQL| B D[Legacy Client] -->|SOAP/XML| B B -->|gRPC| E[Product Service] B -->|REST/JSON| F[User Service] B -->|Messaging| G[Notification Service] style B fill:#f8cecc,stroke:#b85450

// 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.

graph TD A[Clients] --> B[API Gateway] B --> C[Service A] B --> D[Service B] B --> E[Service C] B --> F[Service D] style B fill:#f8cecc,stroke:#b85450

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.

graph TD A[Clients] --> B[Service A Gateway] A --> C[Service B Gateway] A --> D[Service C Gateway] B --> E[Service A] C --> F[Service B] D --> G[Service C] style B,C,D fill:#f8cecc,stroke:#b85450

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.

graph TD A[Mobile App] --> B[Mobile BFF] C[Web App] --> D[Web BFF] E[Third-party API] --> F[Partner BFF] B --> G[Service A] B --> H[Service B] D --> G D --> H D --> I[Service C] F --> G F --> J[Service D] style B,D,F fill:#f8cecc,stroke:#b85450

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.

graph TD A[API Gateway] -->|Lookup| B[Service Registry] B -->|Returns Endpoints| A A -->|Routes to| C[Service Instance 1] A -->|Routes to| D[Service Instance 2] A -->|Routes to| E[Service Instance 3] F[New Service Instance] -->|Registers| B G[Unhealthy Instance] -->|Deregisters| B

// 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.

graph TD A[Global DNS] --> B[US East Gateway] A --> C[EU West Gateway] A --> D[Asia Pacific Gateway] subgraph "US East Region" B --> B1[US East Services] end subgraph "EU West Region" C --> C1[EU West Services] end subgraph "Asia Pacific Region" D --> D1[Asia Pacific Services] end B -.->|Failover| C C -.->|Failover| D D -.->|Failover| B

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:


// 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.

sequenceDiagram participant Client participant Gateway participant ServiceA participant ServiceB participant ServiceC Client->>Gateway: Request (trace-id: abc123) Gateway->>ServiceA: Request (trace-id: abc123, span-id: def456) ServiceA->>ServiceB: Request (trace-id: abc123, span-id: ghi789) ServiceB->>ServiceC: Request (trace-id: abc123, span-id: jkl012) ServiceC-->>ServiceB: Response ServiceB-->>ServiceA: Response ServiceA-->>Gateway: Response Gateway-->>Client: Response

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

Your gateway should implement the following features:

  1. Request routing to the appropriate service
  2. JWT authentication and authorization
  3. Request logging and basic metrics collection
  4. Basic request aggregation for a product details endpoint
  5. 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

When implemented correctly, API Gateways simplify client integration, improve security, and enable independent evolution of backend services while providing a consistent interface to clients.

Additional Resources