Skip to main content
โšก Calmops

API Gateway Patterns: Architecture, Implementation, and Best Practices

API Gateway serves as the single entry point for a group of microservices. This comprehensive guide covers gateway patterns, implementation strategies, security, and operational best practices.

What is an API Gateway?

The Gateway Role

graph TD
    Client1[Mobile App] --> Gateway
    Client2[Web App] --> Gateway
    Client3[Third Party] --> Gateway
    
    Gateway --> Auth[Authentication]
    Gateway --> Rate[Rate Limiting]
    Gateway --> Cache[Caching]
    Gateway --> Route[Routing]
    
    Auth --> Users[/users-service]
    Auth --> Orders[/orders-service]
    Auth --> Products[/products-service]
    Auth --> Payments[/payments-service]
    
    Route --> Cache
    Cache --> DB[(Database)]

Gateway Responsibilities

Responsibility Description
Routing Forward requests to appropriate microservices
Authentication Verify user identity and tokens
Rate Limiting Prevent abuse and ensure fair usage
Caching Reduce backend load with cached responses
Protocol Translation Convert between HTTP, WebSocket, gRPC
Logging/Monitoring Track requests, latency, errors
SSL/TLS Termination Handle HTTPS connections

Gateway vs Direct Service Communication

Without API Gateway

graph LR
    Client --> Users
    Client --> Orders
    Client --> Products
    Client --> Payments
    
    Users --> Auth1[Auth Service]
    Orders --> Auth2[Auth Service]
    Products --> Auth3[Auth Service]
    Payments --> Auth4[Auth Service]
    
    style Client fill:#f9f
    style Users fill:#ff9
    style Orders fill:#ff9
    style Products fill:#ff9
    style Payments fill:#ff9

Problems:

  • Each client must know all service locations
  • Repeated authentication logic
  • No centralized rate limiting
  • CORS issues
  • Limited caching opportunities

With API Gateway

graph LR
    Client --> Gateway[API Gateway]
    
    Gateway --> Auth[Authentication]
    Gateway --> Rate[Rate Limiting]
    Gateway --> Cache[Caching]
    
    Gateway --> Users
    Gateway --> Orders
    Gateway --> Products
    Gateway --> Payments
    
    style Client fill:#f9f
    style Gateway fill:#9f9

Benefits:

  • Single entry point for all clients
  • Centralized cross-cutting concerns
  • Service discovery handled internally
  • Simplified client code
  • Unified security policy

Comparison Table

Gateway Type Pros Cons
Kong Open Source Extensive plugins, Lua Complex setup
NGINX Open Source High performance, proven Limited orchestration
AWS API Gateway Managed Serverless, AWS integration Vendor lock-in, cost
Azure API Management Managed Developer portal, analytics Azure dependency
Apigee Managed Enterprise features Complex pricing
Traefik Open Source Kubernetes native, easy Less mature
Express Gateway Open Source Node.js based, JS Smaller ecosystem

Kong API Gateway Implementation

Installation

# docker-compose.yml
version: '3.8'

services:
  kong:
    image: kong:latest
    environment:
      KONG_DATABASE: postgres
      KONG_PG_HOST: postgres
      KONG_PG_USER: kong
      KONG_PG_PASSWORD: kong
      KONG_PROXY_ACCESS_LOG: /dev/stdout
      KONG_ADMIN_ACCESS_LOG: /dev/stdout
      KONG_PROXY_ERROR_LOG: /dev/stderr
      KONG_ADMIN_ERROR_LOG: /dev/stderr
      KONG_DECLARATIVE_CONFIG: /kong/kong.yml
    ports:
      - "8000:8000"    # Proxy
      - "8443:8443"   # Proxy SSL
      - "8001:8001"   # Admin API
    volumes:
      - ./kong.yml:/kong/kong.yml
    depends_on:
      - postgres
    networks:
      - kong-net

  postgres:
    image: postgres:13
    environment:
      POSTGRES_DB: kong
      POSTGRES_USER: kong
      POSTGRES_PASSWORD: kong
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - kong-net

volumes:
  postgres-data:

networks:
  kong-net:
    driver: bridge

Kong Configuration (declarative)

# kong.yml
_format_version: "3.0"

services:
  - name: user-service
    url: http://user-service:3001
    routes:
      - name: user-route
        paths:
          - /api/users
        methods:
          - GET
          - POST
          - PUT
          - DELETE
    plugins:
      - name: jwt
      - name: rate-limiting
        config:
          minute: 100
          policy: redis
          redis_host: redis
      - name: cors

  - name: order-service
    url: http://order-service:3002
    routes:
      - name: order-route
        paths:
          - /api/orders
    plugins:
      - name: jwt
      - name: rate-limiting
        config:
          minute: 50
      - name: cors

  - name: product-service
    url: http://product-service:3003
    routes:
      - name: product-route
        paths:
          - /api/products
    plugins:
      - name: key-auth
      - name: rate-limiting
        config:
          minute: 200
      - name: cors

consumers:
  - username: mobile-app
    plugins:
      - name: rate-limiting
        config:
          minute: 1000

  - username: web-app
    plugins:
      - name: rate-limiting
        config:
          minute: 500

  - username: trusted-partner
    plugins:
      - name: rate-limiting
        config:
          minute: 10000

NGINX API Gateway

Basic Configuration

# /etc/nginx/nginx.conf

http {
    upstream user_service {
        server user-service-1:3001;
        server user-service-2:3002;
        keepalive 32;
    }
    
    upstream order_service {
        server order-service-1:3001;
        server order-service-2:3002;
        keepalive 32;
    }
    
    # Rate limiting zone
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    
    # API Gateway server
    server {
        listen 80;
        server_name api.example.com;
        
        # Security headers
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        
        # Routes
        location /api/users {
            limit_req zone=api_limit burst=20 nodelay;
            
            # JWT validation
            auth_jwt "" token=$http_authorization;
            auth_jwt_keyfile /etc/nginx/jwt-key.pem;
            
            proxy_pass http://user_service;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            
            # Caching
            proxy_cache_valid 200 5m;
            proxy_cache_key "$scheme$request_method$host$request_uri";
            add_header X-Cache-Status $upstream_cache_status;
        }
        
        location /api/orders {
            limit_req zone=api_limit burst=20 nodelay;
            
            # IP-based access control
            allow 10.0.0.0/8;
            allow 172.16.0.0/12;
            deny all;
            
            proxy_pass http://order_service;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }
        
        location /api/products {
            # Different rate limit for products
            limit_req zone=api_limit burst=50 nodelay;
            
            proxy_pass http://product_service;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }
        
        # Health check endpoint
        location /health {
            access_log off;
            return 200 "healthy\n";
            add_header Content-Type text/plain;
        }
        
        # Rate limit exceeded
        limit_req_status 429;
        error_page 429 = @rate_limit_exceeded;
        
        location @rate_limit_exceeded {
            return 429 '{"error": "Rate limit exceeded", "retry_after": 60}';
            add_header Content-Type application/json;
        }
    }
}

AWS API Gateway

Serverless Implementation

// AWS Lambda + API Gateway handler
const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();

exports.handler = async (event) => {
    const { httpMethod, path, headers, body, queryStringParameters } = event;
    
    // Extract JWT from authorization header
    const token = headers.Authorization?.replace('Bearer ', '');
    
    try {
        switch (true) {
            // GET /users
            case httpMethod === 'GET' && path.match(/^\/users$/):
                return await handleGetUsers(queryStringParameters);
            
            // GET /users/{id}
            case httpMethod === 'GET' && path.match(/^\/users\/(.+)$/):
                const userId = path.match(/^\/users\/(.+)$/)[1];
                return await handleGetUser(userId);
            
            // POST /users
            case httpMethod === 'POST' && path.match(/^\/users$/):
                return await handleCreateUser(JSON.parse(body));
            
            // PUT /users/{id}
            case httpMethod === 'PUT' && path.match(/^\/users\/(.+)$/):
                const updateId = path.match(/^\/users\/(.+)$/)[1];
                return await handleUpdateUser(updateId, JSON.parse(body));
            
            // DELETE /users/{id}
            case httpMethod === 'DELETE' && path.match(/^\/users\/(.+)$/):
                const deleteId = path.match(/^\/users\/(.+)$/)[1];
                return await handleDeleteUser(deleteId);
            
            default:
                return response(404, { error: 'Not found' });
        }
    } catch (error) {
        console.error('Error:', error);
        return response(500, { error: 'Internal server error' });
    }
};

async function handleGetUsers(params) {
    const { limit = 10, nextToken } = params || {};
    
    const result = await dynamoDB.scan({
        TableName: 'users',
        Limit: parseInt(limit),
        ExclusiveStartKey: nextToken ? JSON.parse(Buffer.from(nextToken, 'base64').toString()) : undefined
    }).promise();
    
    return response(200, {
        users: result.Items,
        nextToken: result.LastEvaluatedKey 
            ? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString('base64')
            : null
    });
}

async function handleGetUser(userId) {
    const result = await dynamoDB.get({
        TableName: 'users',
        Key: { id: userId }
    }).promise();
    
    if (!result.Item) {
        return response(404, { error: 'User not found' });
    }
    
    return response(200, result.Item);
}

async function handleCreateUser(data) {
    const { name, email, role = 'user' } = data;
    
    if (!name || !email) {
        return response(400, { error: 'Name and email are required' });
    }
    
    const user = {
        id: require('crypto').randomUUID(),
        name,
        email,
        role,
        createdAt: new Date().toISOString()
    };
    
    await dynamoDB.put({
        TableName: 'users',
        Item: user
    }).promise();
    
    return response(201, user);
}

async function handleUpdateUser(userId, data) {
    const updateExpressions = [];
    const expressionAttributeValues = {};
    
    Object.keys(data).forEach(key => {
        if (key !== 'id') {
            updateExpressions.push(`${key} = :${key}`);
            expressionAttributeValues[`:${key}`] = data[key];
        }
    });
    
    if (updateExpressions.length === 0) {
        return response(400, { error: 'No fields to update' });
    }
    
    const result = await dynamoDB.update({
        TableName: 'users',
        Key: { id: userId },
        UpdateExpression: `SET ${updateExpressions.join(', ')}`,
        ExpressionAttributeValues: expressionAttributeValues,
        ReturnValues: 'ALL_NEW'
    }).promise();
    
    return response(200, result.Attributes);
}

async function handleDeleteUser(userId) {
    await dynamoDB.delete({
        TableName: 'users',
        Key: { id: userId }
    }).promise();
    
    return response(204, null);
}

function response(statusCode, data) {
    return {
        statusCode,
        headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'
        },
        body: data ? JSON.stringify(data) : ''
    };
}

AWS API Gateway Terraform

# main.tf

# API Gateway REST API
resource "aws_api_gateway_rest_api" "main" {
  name        = "my-api-gateway"
  description = "Main API Gateway"
  
  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

# Resource: /users
resource "aws_api_gateway_resource" "users" {
  rest_api_id = aws_api_gateway_rest_api.main.id
  parent_id   = aws_api_gateway_rest_api.main.root_resource_id
  path_part   = "users"
}

# Method: GET /users
resource "aws_api_gateway_method" "users_get" {
  rest_api_id   = aws_api_gateway_rest_api.main.id
  resource_id   = aws_api_gateway_resource.users.id
  http_method   = "GET"
  authorization = "COGNITO_USER_POOLS"
  authorizer_id = aws_api_gateway_authorizer.main.id
}

# Integration with Lambda
resource "aws_api_gateway_integration" "users_lambda" {
  rest_api_id = aws_api_gateway_rest_api.main.id
  resource_id = aws_api_gateway_resource.users.id
  http_method = aws_api_gateway_method.users_get.http_method

  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.main.invoke_arn
}

# Usage Plan
resource "aws_api_gateway_usage_plan" "main" {
  name = "api-usage-plan"

  api_stages {
    api_id = aws_api_gateway_rest_api.main.id
    stage  = aws_api_gateway_stage.prod.stage_name
  }

  quota_settings {
    limit  = 1000000
    period = "MONTH"
  }

  throttle_settings {
    burst_limit = 5000
    rate_limit = 1000
  }
}

# API Key
resource "aws_api_gateway_api_key" "main" {
  name = "my-api-key"
}

resource "aws_api_gateway_usage_plan_key" "main" {
  key_id        = aws_api_gateway_api_key.main.id
  key_type      = "API_KEY"
  usage_plan_id = aws_api_gateway_usage_plan.main.id
}

# Stage
resource "aws_api_gateway_stage" "prod" {
  deployment_id = aws_api_gateway_deployment.main.id
  rest_api_id   = aws_api_gateway_rest_api.main.id
  stage_name    = "prod"

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.api_gateway.arn
    format         = "$context.requestId: $context.endpoint $context.httpMethod $context.status $context.responseLatency"
  }
}

# Deployment
resource "aws_api_gateway_deployment" "main" {
  rest_api_id = aws_api_gateway_rest_api.main.id

  lifecycle {
    create_before_destroy = true
  }
}

Authentication Patterns

JWT Validation

// JWT validation middleware (Express.js)
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const jwksUri = process.env.JWKS_URI || 'https://auth.example.com/.well-known/jwks.json';
const audience = process.env.AUDIENCE;
const issuer = process.env.ISSUER;

const client = jwksClient({
  jwksUri,
  cache: true,
  cacheMaxAge: 600000
});

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    if (err) {
      return callback(err);
    }
    const signingKey = key.getPublicKey();
    callback(null, signingKey);
  });
}

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  const token = authHeader.split(' ')[1];
  
  jwt.verify(token, getKey, {
    audience,
    issuer,
    algorithms: ['RS256']
  }, (err, decoded) => {
    if (err) {
      return res.status(401).json({ error: 'Invalid token', details: err.message });
    }
    
    req.user = decoded;
    next();
  });
}

// Usage
app.get('/api/protected', authMiddleware, (req, res) => {
  res.json({ 
    message: 'Access granted',
    user: req.user 
  });
});

OAuth2 Integration

// OAuth2 token introspection
const axios = require('axios');

async function introspectToken(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader) {
    return res.status(401).json({ error: 'No authorization header' });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    // Introspect with authorization server
    const response = await axios.post(
      process.env.TOKEN_INTROSPECTION_URL,
      `token=${token}`,
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Authorization': `Basic ${Buffer.from(
            process.env.CLIENT_ID + ':' + process.env.CLIENT_SECRET
          ).toString('base64')}`
        }
      }
    );
    
    const { active, scope, client_id, exp } = response.data;
    
    if (!active) {
      return res.status(401).json({ error: 'Token inactive' });
    }
    
    // Check expiration
    if (exp * 1000 < Date.now()) {
      return res.status(401).json({ error: 'Token expired' });
    }
    
    req.tokenInfo = { scope, client_id, exp };
    next();
    
  } catch (error) {
    console.error('Token introspection failed:', error);
    return res.status(500).json({ error: 'Token validation failed' });
  }
}

// Scope-based access control
function requireScope(...requiredScopes) {
  return (req, res, next) => {
    const tokenScopes = req.tokenInfo?.scope?.split(' ') || [];
    
    const hasScope = requiredScopes.every(scope => tokenScopes.includes(scope));
    
    if (!hasScope) {
      return res.status(403).json({ 
        error: 'Insufficient permissions',
        required: requiredScopes,
        actual: tokenScopes
      });
    }
    
    next();
  };
}

// Usage
app.get('/api/admin',
  requireScope('read:admin', 'write:admin'),
  adminHandler
);

Rate Limiting Implementation

Token Bucket Algorithm

// Token bucket rate limiter
class TokenBucket {
  constructor(rate, capacity) {
    this.rate = rate;        // Tokens per second
    this.capacity = capacity; // Max tokens in bucket
    this.tokens = capacity;
    this.lastRefill = Date.now();
  }
  
  consume(tokens = 1) {
    this.refill();
    
    if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return { allowed: true, remaining: this.tokens };
    }
    
    const waitTime = ((tokens - this.tokens) / this.rate) * 1000;
    return { 
      allowed: false, 
      remaining: this.tokens,
      retryAfter: Math.ceil(waitTime / 1000)
    };
  }
  
  refill() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    const tokensToAdd = elapsed * this.rate;
    
    this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
    this.lastRefill = now;
  }
}

// Distributed rate limiter with Redis
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

async function distributedRateLimiter(key, limit, window) {
  const current = await redis.incr(key);
  
  if (current === 1) {
    await redis.expire(key, window);
  }
  
  const ttl = await redis.ttl(key);
  
  if (current > limit) {
    return {
      allowed: false,
      remaining: 0,
      retryAfter: ttl,
      limit,
      reset: Math.floor(Date.now() / 1000) + ttl
    };
  }
  
  return {
    allowed: true,
    remaining: limit - current,
    limit,
    reset: Math.floor(Date.now() / 1000) + ttl
  };
}

// Express middleware
function rateLimitMiddleware(req, res, next) {
  const apiKey = req.headers['x-api-key'] || req.ip;
  const limit = req.user?.tier === 'premium' ? 1000 : 100;
  const window = 60; // seconds
  
  const limiter = new TokenBucket(limit / window, limit);
  const result = limiter.consume();
  
  // Set rate limit headers
  res.set({
    'X-RateLimit-Limit': limit,
    'X-RateLimit-Remaining': result.remaining,
    'X-RateLimit-Reset': Math.floor(Date.now() / 1000) + window
  });
  
  if (!result.allowed) {
    res.set('Retry-After', result.retryAfter);
    return res.status(429).json({
      error: 'Too many requests',
      retryAfter: result.retryAfter
    });
  }
  
  next();
}

Caching Strategies

Multi-layer Caching

// CDN + API Gateway + Service caching
const NodeCache = require('node-cache');

// In-memory cache with TTL
const cache = new NodeCache({ stdTTL: 300 });

// Cache key generator
function cacheKey(method, path, params, userId) {
  return `${method}:${path}:${JSON.stringify(params)}:${userId}`;
}

// Cache-aside pattern
async function cacheAside(key, fetchFn, ttl = 300) {
  // Check cache first
  const cached = cache.get(key);
  if (cached) {
    return { data: cached, source: 'cache' };
  }
  
  // Fetch from source
  const data = await fetchFn();
  
  // Store in cache
  if (data) {
    cache.set(key, data, ttl);
  }
  
  return { data, source: 'origin' };
}

// Vary by headers
function varyCacheKey(baseKey, varyHeaders) {
  const vary = varyHeaders
    .map(h => `${h}:${req.headers[h] || 'none'}`)
    .join(':');
  return `${baseKey}:${vary}`;
}

// Stale-while-revalidate
async function staleWhileRevalidate(key, fetchFn, ttl = 300) {
  const cached = cache.get(key);
  
  if (cached) {
    // Return cached immediately
    Promise.resolve().then(async () => {
      try {
        const fresh = await fetchFn();
        cache.set(key, fresh, ttl);
      } catch (e) {
        console.error('Background refresh failed:', e);
      }
    });
    
    return { data: cached, stale: false };
  }
  
  const data = await fetchFn();
  cache.set(key, data, ttl);
  
  return { data, stale: false };
}

Circuit Breaker Pattern

// Circuit breaker implementation
class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.successThreshold = options.successThreshold || 2;
    this.timeout = options.timeout || 60000;
    this.state = 'CLOSED';
    this.failures = 0;
    this.successes = 0;
    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.failures = 0;
    
    if (this.state === 'HALF_OPEN') {
      this.successes++;
      if (this.successes >= this.successThreshold) {
        this.state = 'CLOSED';
        this.successes = 0;
      }
    }
  }
  
  onFailure() {
    this.failures++;
    this.successes = 0;
    
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.timeout;
    }
  }
  
  getState() {
    return this.state;
  }
}

// Usage with microservices
const userServiceBreaker = new CircuitBreaker({
  failureThreshold: 5,
  timeout: 30000
});

async function callUserService(endpoint) {
  return userServiceBreaker.execute(async () => {
    const response = await axios.get(`http://user-service:3001${endpoint}`);
    return response.data;
  });
}

Monitoring and Observability

Request Logging

// Morgan-style request logging
function loggingMiddleware(req, res, next) {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    
    const log = {
      timestamp: new Date().toISOString(),
      method: req.method,
      url: req.originalUrl,
      status: res.statusCode,
      duration,
      size: res.get('Content-Length') || 0,
      ip: req.ip,
      userAgent: req.get('user-agent'),
      requestId: req.id || 'unknown'
    };
    
    // Send to logging service
    console.log(JSON.stringify(log));
    
    // Or send to ELK/Splunk
    // elasticsearch.index({ index: 'api-logs', body: log });
  });
  
  next();
}

// Metrics collection
const metrics = {
  requests: new Counter('api_requests_total', 'Total API requests', ['method', 'endpoint', 'status']),
  latency: new Histogram('api_request_duration_ms', 'API request latency', ['method', 'endpoint']),
  active: new Gauge('api_active_requests', 'Active API requests')
};

function metricsMiddleware(req, res, next) {
  const start = Date.now();
  const endpoint = req.route?.path || req.path;
  
  metrics.requests.inc({ method: req.method, endpoint, status: 'started' });
  metrics.active.inc({ method: req.method, endpoint });
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    
    metrics.requests.inc({ method: req.method, endpoint, status: res.statusCode });
    metrics.latency.observe({ method: req.method, endpoint }, duration);
    metrics.active.dec({ method: req.method, endpoint });
  });
  
  next();
}

External Resources

Comments