Skip to content
All essays
ArchitectureMarch 27, 202416 min

API Gateway Complete Guide

Master API Gateway patterns for building scalable microservices architectures.

Ü
Ümit Uz
Mobile & Full Stack Developer

An API Gateway is the front door for your microservices, handling cross-cutting concerns and providing a unified interface to clients. Here's how to implement one effectively.

What Is an API Gateway?

An API Gateway sits between clients and backend services, handling routing, composition, and cross-cutting concerns like auth, rate limiting, and caching.

text
┌─────────┐
│ Client  │
└────┬────┘
     │
     ▼
┌─────────────┐
│ API Gateway │ ← Single entry point
└──────┬──────┘
       │
       ├──────────┬──────────┬──────────┐
       ▼          ▼          ▼          ▼
   ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐
   │User  │  │Order │  │Payment│  │Product│
   │Service│  │Service│ │Service│ │Service│
   └──────┘  └──────┘  └──────┘  └──────┘

Core Responsibilities

Request Routing

javascript
// Express-based API Gateway
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// Route to user service
app.use('/api/users', createProxyMiddleware({
  target: 'http://users-service:3001',
  changeOrigin: true,
}));

// Route to order service
app.use('/api/orders', createProxyMiddleware({
  target: 'http://orders-service:3002',
  changeOrigin: true,
}));

// Route to product service
app.use('/api/products', createProxyMiddleware({
  target: 'http://products-service:3003',
  changeOrigin: true,
}));

app.listen(3000);

Authentication and Authorization

javascript
const jwt = require('jsonwebtoken');

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid token' });
    }

    req.user = user;
    next();
  });
}

// Apply to protected routes
app.use('/api/*', authenticateToken);

Rate Limiting

javascript
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP',
});

// Apply to all requests
app.use(limiter);

// Stricter limits for expensive endpoints
const strictLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: 'Rate limit exceeded for this endpoint',
});

app.post('/api/orders', strictLimiter);

API Composition

BFF Pattern (Backend for Frontend)

Different gateways for different clients.

javascript
// Gateway for Web
app.get '/api/web/products/:id', async (req, res) => {
  const product = await fetchProduct(req.params.id);
  const reviews = await fetchReviews(req.params.id);
  const recommendations = await fetchRecommendations(req.params.id);

  res.json({
    product,
    reviews,
    recommendations,
  });
}

// Gateway for Mobile (lighter response)
app.get('/api/mobile/products/:id', async (req, res) => {
  const product = await fetchProduct(req.params.id);
  const reviews = await fetchReviews(req.params.id, { limit: 5 });

  res.json({
    product,
    reviews,
  });
}

Aggregation

Combine multiple service responses.

javascript
app.get('/api/dashboard', async (req, res) => {
  try {
    const [stats, orders, users] = await Promise.all([
      fetch('http://stats-service/stats').then(r => r.json()),
      fetch('http://orders-service/orders').then(r => r.json()),
      fetch('http://users-service/users').then(r => r.json()),
    ]);

    res.json({
      stats: stats.total,
      recentOrders: orders.slice(0, 10),
      activeUsers: users.filter(u => u.active).length,
    });
  } catch (error) {
    res.status(500).json({ error: 'Service unavailable' });
  }
});

Caching Strategies

Response Caching

javascript
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes

app.get('/api/products/:id', async (req, res) => {
  const cacheKey = `product:${req.params.id}`;

  // Check cache first
  const cached = cache.get(cacheKey);
  if (cached) {
    console.log('Cache hit');
    return res.json(cached);
  }

  // Fetch from service
  const product = await fetch(`http://products-service/products/${req.params.id}`)
    .then(r => r.json());

  // Store in cache
  cache.set(cacheKey, product);

  res.json(product);
});

Cache Invalidation

javascript
// Invalidate on updates
app.put('/api/products/:id', async (req, res) => {
  const updated = await fetch(`http://products-service/products/${req.params.id}`, {
    method: 'PUT',
    body: JSON.stringify(req.body),
  }).then(r => r.json());

  // Invalidate cache
  cache.del(`product:${req.params.id}`);

  res.json(updated);
});

Load Balancing

Round-Robin

javascript
const services = [
  'http://service1:3000',
  'http://service2:3000',
  'http://service3:3000',
];

let currentIndex = 0;

function getNextService() {
  const service = services[currentIndex];
  currentIndex = (currentIndex + 1) % services.length;
  return service;
}

app.use('/api/users', (req, res) => {
  const target = getNextService();
  proxy.web(req, res, { target });
});

Least Connections

javascript
const serviceConnections = new Map();

function getServiceWithLeastConnections() {
  let minService = services[0];
  let minConnections = Infinity;

  for (const service of services) {
    const connections = serviceConnections.get(service) || 0;
    if (connections < minConnections) {
      minConnections = connections;
      minService = service;
    }
  }

  return minService;
}

app.use('/api/users', (req, res) => {
  const service = getServiceWithLeastConnections();

  // Increment connections
  serviceConnections.set(service, (serviceConnections.get(service) || 0) + 1);

  proxy.web(req, res, { target }, () => {
    // Decrement when done
    serviceConnections.set(service, serviceConnections.get(service) - 1);
  });
});

Circuit Breaker

javascript
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    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.failureCount = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.timeout;
    }
  }
}

// Usage
const circuitBreaker = new CircuitBreaker();

app.get('/api/users', async (req, res) => {
  try {
    const users = await circuitBreaker.execute(async () => {
      return fetch('http://users-service/users').then(r => r.json());
    });
    res.json(users);
  } catch (error) {
    res.status(503).json({ error: 'Service unavailable' });
  }
});

Request/Response Transformation

Request Transformation

javascript
function transformRequest(req, res, next) {
  // Add headers
  req.headers['X-Request-ID'] = generateId();
  req.headers['X-Forwarded-For'] = req.ip;

  // Transform body
  if (req.body) {
    req.body = {
      ...req.body,
      timestamp: new Date().toISOString(),
      gateway: 'api-gateway-v1',
    };
  }

  next();
}

app.use('/api/*', transformRequest);

Response Transformation

javascript
function transformResponse(req, res, next) {
  const originalSend = res.send;

  res.send = function(data) {
    // Envelope response
    const response = {
      success: res.statusCode < 400,
      data,
      meta: {
        requestId: req.headers['X-Request-ID'],
        timestamp: new Date().toISOString(),
      },
    };

    originalSend.call(this, response);
  };

  next();
}

app.use('/api/*', transformResponse);

Monitoring and Logging

Request Logging

javascript
const morgan = require('morgan');

// Log requests
app.use(morgan('combined'));

// Custom logging
app.use((req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`
      ${req.method} ${req.url}
      Status: ${res.statusCode}
      Duration: ${duration}ms
    `);
  });

  next();
});

Metrics Collection

javascript
const promClient = require('prom-client');

const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
});

app.use((req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    httpRequestDuration.observe(
      {
        method: req.method,
        route: req.route?.path || req.url,
        status_code: res.statusCode,
      },
      duration
    );
  });

  next();
});

// Metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', promClient.register.contentType);
  res.end(await promClient.register.metrics());
});

Kong

yaml
# Kong configuration
services:
  - name: user-service
    url: http://users-service:3001

routes:
  - name: user-routes
    service: user-service
    paths:
      - /api/users

AWS API Gateway

javascript
// Serverless framework
provider:
  name: aws
  runtime: nodejs18.x

functions:
  getUsers:
    handler: handler.getUsers
    events:
      - http:
          path: users
          method: get
          cors: true

Traefik

yaml
# traefik.yml
http:
  routers:
    user-service:
      rule: "PathPrefix(`/api/users`)"
      service: user-service

  services:
    user-service:
      loadBalancer:
        servers:
          - url: "http://users-service:3001"

Best Practices

DOs

  • Use API Gateway for cross-cutting concerns
  • Implement proper authentication and authorization
  • Add rate limiting to prevent abuse
  • Cache responses where appropriate
  • Monitor and log all requests
  • Implement circuit breakers for resilience
  • Use HTTPS for all communication

DON'Ts

  • Create a monolithic gateway
  • Add business logic to the gateway
  • Overly complicate routing rules
  • Ignore performance implications
  • Skip error handling
  • Forget about versioning
  • Make the gateway a bottleneck

Conclusion

API Gateway is essential for microservices architecture. It provides a unified interface, handles cross-cutting concerns, and enables independent service evolution. Start with basic routing and auth, then add features like caching, rate limiting, and monitoring as needed. The right balance depends on your specific requirements.

Enjoyed this essay? Share it.

Related essays

Next essay
Backend for Frontend (BFF) Pattern