Node.js
June 10, 20267 min read...
Node.jsJune 10, 20267 min read

Node.js Microservices: Building an API Gateway with Express Gateway

Architect scalable microservices in Node.js with an API Gateway pattern. Learn routing, service discovery, JWT authentication propagation, and circuit breakers using Express Gateway and http-proxy-middleware.

Node.js Microservices: Building an API Gateway with Express Gateway

Microservices promise independent scaling and deployment, but they introduce complexity: client applications need to know the location of 10+ services, authentication must be orchestrated, and rate limiting becomes distributed. The API Gateway pattern solves these challenges by providing a single entry point. This guide shows you how to build a production-ready gateway in Node.js.

Why an API Gateway?

An API Gateway sits between clients and microservices, handling:

  • Request routing to appropriate service
  • Authentication and authorization
  • Rate limiting and throttling
  • Response aggregation (multiple services into one response)
  • Observability (logging, metrics, tracing)

Building a Gateway with http-proxy-middleware

Basic proxy setup that routes to different services based on URL path:

import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';

const app = express();

// Route /api/users -> user-service:3001 app.use(‘/api/users’, createProxyMiddleware({ target: ‘http://user-service:3001’, changeOrigin: true, pathRewrite: { ‘^/api/users’: ‘’ }, onProxyReq: (proxyReq, req, res) => { // Forward original user ID from auth header if (req.userId) { proxyReq.setHeader(‘X-User-Id’, req.userId); } } }));

// Route /api/orders -> order-service:3002 app.use(‘/api/orders’, createProxyMiddleware({ target: ‘http://order-service:3002’, changeOrigin: true, pathRewrite: { ‘^/api/orders’: ‘’ } }));

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

app.listen(8080);

Service Discovery with Consul or etcd

Hardcoded service URLs break in dynamic environments. Use service discovery:

import Consul from 'consul';

const consul = new Consul({ host: ‘consul-server’, port: 8500 });

async function getServiceUrl(serviceName) { const services = await consul.catalog.service.nodes(serviceName); if (!services.length) throw new Error(No nodes for ${serviceName});

// Simple round-robin or random selection const node = services[Math.floor(Math.random() * services.length)]; return http://${node.ServiceAddress}:${node.ServicePort}; }

// Dynamic proxy with service lookup app.use(‘/api/users’, async (req, res, next) => { const target = await getServiceUrl(‘user-service’); const proxy = createProxyMiddleware({ target, changeOrigin: true }); proxy(req, res, next); });

Authentication at Gateway Level

Centralize JWT verification so individual services don't have to:

import jwt from 'jsonwebtoken';

const authMiddleware = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader) return res.status(401).json({ error: ‘Missing token’ });

const token = authHeader.split(’ ')[1]; try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; // Attach to request next(); } catch (err) { res.status(403).json({ error: ‘Invalid token’ }); } };

// Protect all routes under /api app.use(‘/api’, authMiddleware);

// Now downstream services receive user info via headers const proxyWithUser = createProxyMiddleware({ target: ‘http://user-service:3001’, changeOrigin: true, onProxyReq: (proxyReq, req) => { proxyReq.setHeader(‘X-User-Id’, req.user.userId); proxyReq.setHeader(‘X-User-Roles’, JSON.stringify(req.user.roles)); } });

Rate Limiting per Client and per Service

Use Redis-based rate limiting at gateway to protect downstream services:

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';

// Global limit (all endpoints) const globalLimiter = rateLimit({ store: new RedisStore({ client: redis }), windowMs: 60 * 1000, max: 1000, // 1000 requests per minute per IP keyGenerator: (req) => req.ip });

// Per-user stricter limit for sensitive endpoints const authLimiter = rateLimit({ store: new RedisStore({ client: redis }), windowMs: 15 * 60 * 1000, max: 20, // 20 login attempts keyGenerator: (req) => req.body.email || req.ip });

app.use(‘/api’, globalLimiter); app.use(‘/api/auth/login’, authLimiter);

Response Aggregation (GraphQL Federation)

Instead of client making 3 round trips, gateway fetches from multiple services and combines:

app.get('/api/user-dashboard/:userId', async (req, res) => {
  const userId = req.params.userId;

// Parallel requests to multiple services const [profile, orders, recommendations] = await Promise.all([ fetch(${USER_SERVICE}/users/${userId}).then(r => r.json()), fetch(${ORDER_SERVICE}/orders?userId=${userId}).then(r => r.json()), fetch(${RECOMMENDATION_SERVICE}/recs/${userId}).then(r => r.json()) ]);

res.json({ user: profile, recentOrders: orders.slice(0, 5), recommendations }); });

Circuit Breaker Pattern

Prevent cascading failures when a service is down:

import CircuitBreaker from 'opossum';

const options = { timeout: 3000, errorThresholdPercentage: 50, // Open circuit if 50% fail resetTimeout: 30000, // Try again after 30 seconds };

async function callUserService(req) { const response = await fetch(‘http://user-service:3001/users/me’, { headers: { Authorization: req.headers.authorization } }); if (!response.ok) throw new Error(‘Service error’); return response.json(); }

const breaker = new CircuitBreaker(callUserService, options);

app.get(‘/api/me’, async (req, res) => { try { const user = await breaker.fire(req); res.json(user); } catch (err) { if (breaker.opened) { res.status(503).json({ error: ‘User service unavailable, try later’ }); } else { res.status(500).json({ error: ‘Internal error’ }); } } });

Observability: Logging and Tracing

Add request ID propagation across services:

import { v4 as uuidv4 } from 'uuid';

app.use((req, res, next) => { req.requestId = req.headers[‘x-request-id’] || uuidv4(); res.setHeader(‘X-Request-Id’, req.requestId);

const start = Date.now(); res.on(‘finish’, () => { console.log(JSON.stringify({ requestId: req.requestId, method: req.method, url: req.url, status: res.statusCode, duration: Date.now() - start })); }); next(); });

// Proxy middleware forwards request ID const proxy = createProxyMiddleware({ target: ‘http://service’, onProxyReq: (proxyReq, req) => { proxyReq.setHeader(‘X-Request-Id’, req.requestId); } });

Deployment: Docker Compose and Kubernetes

Example docker-compose for local microservices with gateway:

version: '3'
services:
  gateway:
    build: ./api-gateway
    ports:
      - "8080:8080"
    environment:
      - USER_SERVICE=http://user-service:3001
      - ORDER_SERVICE=http://order-service:3002

user-service: build: ./user-service ports: - “3001”

order-service: build: ./order-service ports: - “3002”

redis: image: redis:7-alpine ports: - “6379”

consul: image: consul:latest ports: - “8500”

Common Pitfalls

  • Gateway becomes bottleneck: Scale gateway horizontally (multiple instances behind load balancer).
  • Timeouts misconfiguration: Gateway timeout should be higher than slowest service + retries.
  • No fallback for service failures: Implement circuit breakers and stale cache fallbacks.
  • Over-aggregation: Returning too much data slows gateway. Consider GraphQL for client-driven fetching.

Alternative Gateway Solutions

  • Express Gateway (open source): Full-featured, plugin-based, but less flexible.
  • Kong: Nginx-based, high-performance, Lua plugins.
  • Traefik: Kubernetes-native with automatic service discovery.
  • Envoy + Ambassador: Very high performance but complex.

Conclusion

A Node.js API Gateway is perfectly capable of handling moderate scale (10k-50k req/s) with proper design. Use http-proxy-middleware for simple routing, add JWT authentication, Redis-based rate limiting, and circuit breakers. For service discovery, integrate Consul or etcd. As traffic grows, scale gateways horizontally and consider moving to Nginx/Kong for the edge layer. Start simple — your services will thank you for the unified entry point.

Comments

Join the conversation — sign in to leave a comment.