← Back to Tutorials
Node.js

Build an API Gateway

Difficulty: Advanced Est. Time: ~4 hours

Introduction

An API gateway is a server that acts as a single entry point for a set of microservices. It handles request routing, composition, authentication, and rate limiting.

In this tutorial, we'll build "APIGateway" - a complete API gateway with routing, authentication, rate limiting, and request transformation.

What You'll Build
  • Central API gateway server
  • Dynamic request routing
  • JWT authentication
  • Rate limiting per client
  • Request/response transformation

Core Concepts

Understanding API gateway architecture.

Gateway Functions

  • Routing - Direct requests to services
  • Aggregation - Combine multiple requests
  • Authentication - Verify credentials
  • Rate Limiting - Control API usage

Project Setup

Bash
Bash
mkdir api-gateway
cd api-gateway
npm init -y
npm install express http-proxy-middleware jwt cors helmet

Gateway Core

JavaScript
// gateway.js
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');

class APIGateway {
    constructor() {
        this.app = express();
        this.routes = new Map();
        this.middleware = [];
        
        this.setupMiddleware();
    }
    
    setupMiddleware() {
        this.app.use(helmet());
        this.app.use(cors());
        this.app.use(express.json());
    }
    
    addRoute(path, service, auth = false) {
        this.routes.set(path, { service, auth });
    }
    
    registerMiddleware(fn) {
        this.middleware.push(fn);
    }
    
    handleRequest(req, res) {
        let matchedRoute = null;
        
        for (const [path, route] of this.routes.entries()) {
            if req.path.startsWith(path) {
                matchedRoute = { path, ...route };
                break;
            }
        }
        
        if (!matchedRoute) {
            return res.status(404).json({ error: 'Route not found' });
        }
        
        const targetPath = req.path.replace(matchedRoute.path, '');
        req.url = targetPath;
        
        req.headers['x-gateway-route'] = matchedRoute.service;
        
        this.proxyRequest(req, res, matchedRoute.service);
    }
    
    proxyRequest(req, res, service) {
        const services = {
            users: 'http://localhost:3001',
            products: 'http://localhost:3002',
            orders: 'http://localhost:3003'
        };
        
        const target = services[service];
        
        if (!target) {
            return res.status(502).json({ error: 'Service unavailable' });
        }
        
        // Simple proxy implementation
        const http = require('http');
        
        const proxyReq = http.request({
            hostname: target.replace('http://', '').split(':')[0],
            port: parseInt(target.split(':')[2]) || 80,
            path: req.url,
            method: req.method,
            headers: req.headers
        }, (proxyRes) => {
            res.writeHead(proxyRes.statusCode, proxyRes.headers);
            proxyRes.pipe(res);
        });
        
        req.pipe(proxyReq);
    }
    
    start(port) {
        for (const fn of this.middleware) {
            this.app.use(fn);
        }
        
        this.app.all('*', (req, res) => this.handleRequest(req, res));
        
        this.app.listen(port, () => {
            console.log(`API Gateway running on port ${port}`);
        });
    }
}

const gateway = new APIGateway();

gateway.addRoute('/api/users', 'users', true);
gateway.addRoute('/api/products', 'products', true);
gateway.addRoute('/api/orders', 'orders', true);

gateway.start(8080);

Request Routing

JavaScript
// router.js
class RouteManager {
    constructor() {
        this.routes = [];
        this.wildcardRoutes = [];
    }
    
    addRoute(config) {
        if config.path.includes('*')) {
            this.wildcardRoutes.push(this.compileRoute(config));
        } else {
            this.routes.push(this.compileRoute(config));
        }
    }
    
    compileRoute(config) {
        return {
            ...config,
            regex: new RegExp(`^${config.path.replace(/:(\w+), '(?<$1>[^/]+)')}$`),
            params: (config.path.match(/:(\w+)/g) || []).map(p => p.slice(1))
        };
    }
    
    match(method, path) {
        for (const route of this.routes) {
            if route.method === method && route.regex.test(path)) {
                const match = route.regex.exec(path);
                return { route, params: match.groups || {} };
            }
        }
        
        return null;
    }
    
    transformRequest(req, route) {
        if (route.requestTransform) {
            req.body = route.requestTransform(req.body);
        }
        
        if (route.addHeaders) {
            Object.assign(req.headers, route.addHeaders);
        }
    }
    
    transformResponse(res, route, data) {
        if (route.responseTransform) {
            return route.responseTransform(data);
        }
        return data;
    }
}

Authentication

JavaScript
// auth.js
const jwt = require('jsonwebtoken');

class AuthMiddleware {
    constructor(secret) {
        this.secret = secret;
    }
    
    verifyToken(req, res, next) {
        const authHeader = req.headers.authorization;
        
        if (!authHeader) {
            return res.status(401).json({ error: 'No token provided' });
        }
        
        const token = authHeader.split(' ')[1];
        
        try {
            const decoded = jwt.verify(token, this.secret);
            req.user = decoded;
            next();
        } catch (err) {
            return res.status(401).json({ error: 'Invalid token' });
        }
    }
    
    requireRole(...roles) {
        return (req, res, next) => {
            if (!req.user) {
                return res.status(401).json({ error: 'Not authenticated' });
            }
            
            if (!roles.includes(req.user.role)) {
                return res.status(403).json({ error: 'Insufficient permissions'});
            }
            
            next();
        };
    }
}

Rate Limiting

JavaScript
// rateLimiter.js
class RateLimiter {
    constructor(options = {}) {
        this.windowMs = options.windowMs || 60000;
        this.maxRequests = options.maxRequests || 100;
        this.store = new Map();
    }
    
    getKey(req) {
        return req.user?.id || req.ip || 'anonymous';
    }
    
    isAllowed(key) {
        const now = Date.now();
        const windowStart = now - this.windowMs;
        
        if (!this.store.has(key)) {
            this.store.set(key, []);
        }
        
        const requests = this.store.get(key).filter(t => t > windowStart);
        
        if (requests.length >= this.maxRequests) {
            return { allowed: false, remaining: 0 };
        }
        
        requests.push(now);
        this.store.set(key, requests);
        
        return { 
            allowed: true, 
            remaining: this.maxRequests - requests.length 
        };
    }
    
    middleware() {
        return (req, res, next) => {
            const key = this.getKey(req);
            const { allowed, remaining } = this.isAllowed(key);
            
            res.set('X-RateLimit-Limit', this.maxRequests);
            res.set('X-RateLimit-Remaining', remaining);
            
            if (!allowed) {
                return res.status(429).json({ error: 'Rate limit exceeded' });
            }
            
            next();
        };
    }
}

Testing

Bash
# Test the API Gateway

# Access routes
curl http://localhost:8080/api/users
curl http://localhost:8080/api/products

# With authentication
TOKEN=$(curl -s -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password"}' | jq -r .token)

curl http://localhost:8080/api/users \
  -H "Authorization: Bearer $TOKEN"
Testing Checklist
  • Routes are correctly routed to services
  • Authentication is enforced
  • Rate limiting works
  • Response transformation works

Summary

You've built a complete API gateway.

What You Built

  • Gateway Core - Central entry point
  • Dynamic Routing - Service routing
  • Authentication - JWT verification
  • Rate Limiting - Per-client limits

Continue Learning

  • Build a Search Index
  • Build a Distributed Cache