API Gateway Patterns & BFF Architecture in Node.js Microservices (2026)

When clients (React Web apps, iOS/Android mobile apps, Smart TVs) directly communicate with dozens of individual microservices, systems face significant friction: excessive client-side network roundtrips, tight coupling to downstream services, security credential sprawl, and high API payload overhead.

To solve this, modern microservice architectures deploy an API Gateway at the edge, often paired with Backend-For-Frontend (BFF) services tailored to specific client applications.

API Gateway vs. Backend-For-Frontend (BFF)

Architecture Pattern Primary Purpose Ownership Core Responsibilities
API Gateway Single edge entry point for centralized cross-cutting concerns Platform / Core Infrastructure Team SSL termination, global rate limiting, IP whitelisting, authentication, protocol translation (gRPC to REST)
BFF (Backend-For-Frontend) Client-specific aggregation layer providing customized API contracts Dedicated Frontend / Mobile Feature Teams Payload stripping, multi-service data aggregation, device-specific caching, UI-centric formatting

Core API Gateway Responsibilities

  1. Reverse Proxy & Request Routing: Maps client routes (e.g., /api/v1/orders) directly to dynamic internal cluster IP addresses via service discovery.
  2. Authentication & Token Offloading: Validates incoming JWTs/OAuth2 tokens at the edge, converting them into lightweight HTTP headers (e.g., x-user-id) sent to downstream services.
  3. Rate Limiting & Resilience: Uses Redis sliding-window algorithms to block abusive clients and circuit breakers to isolate failing services.

1. Implementing a Gateway / BFF in Node.js (Express & Redis)

Below is a production-grade edge gateway script handling JWT authentication offloading, Redis rate limiting, and parallel response aggregation.

import express from 'express';
import httpProxy from 'express-http-proxy';
import jwt from 'jsonwebtoken';
import Redis from 'ioredis';
import axios from 'axios';

const app = express();
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');

app.use(express.json());

// ==========================================
// 1. MIDDLEWARE: REDIS SLIDING WINDOW RATE LIMITER
// ==========================================
const rateLimiter = async (req, res, next) => {
  const clientIp = req.ip;
  const key = `ratelimit:${clientIp}`;
  const now = Date.now();
  const windowSize = 60 * 1000; // 1 minute
  const maxRequests = 100;

  try {
    const multi = redis.multi();
    multi.zremrangebyscore(key, 0, now - windowSize);
    multi.zadd(key, now, now.toString());
    multi.zcard(key);
    multi.expire(key, 60);
    const results = await multi.exec();

    const requestCount = results[2][1];
    if (requestCount > maxRequests) {
      return res.status(429).json({ error: 'Too Many Requests. Rate limit exceeded.' });
    }
    next();
  } catch (err) {
    next(); // Fail open if Redis is down
  }
};

app.use(rateLimiter);

// ==========================================
// 2. MIDDLEWARE: AUTHENTICATION OFFLOADING
// ==========================================
const authenticateEdge = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing Bearer Token' });
  }

  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    // Inject decoded identity into headers for downstream microservices
    req.headers['x-user-id'] = decoded.userId;
    req.headers['x-user-role'] = decoded.role;
    next();
  } catch (err) {
    return res.status(403).json({ error: 'Invalid or Expired Token' });
  }
};

// ==========================================
// 3. REVERSE PROXY ROUTING
// ==========================================
app.use('/api/v1/payments', authenticateEdge, httpProxy('http://payment-service:4002'));
app.use('/api/v1/inventory', authenticateEdge, httpProxy('http://inventory-service:4003'));

// ==========================================
// 4. BFF PATTERN: AGGREGATE DASHBOARD ENDPOINT
// ==========================================
// Reduces 3 client HTTP requests into 1 parallel request at the edge
app.get('/api/v1/mobile-dashboard', authenticateEdge, async (req, res) => {
  const userId = req.headers['x-user-id'];

  try {
    const [userProfile, activeOrders, notifications] = await Promise.all([
      axios.get(`http://user-service:4001/users/${userId}`),
      axios.get(`http://order-service:4004/orders?userId=${userId}&status=ACTIVE`),
      axios.get(`http://notification-service:4005/notifications?userId=${userId}&limit=5`)
    ]);

    // Format & strip fields specifically for mobile view performance
    res.json({
      user: {
        name: userProfile.data.name,
        avatar: userProfile.data.avatarUrl,
      },
      ordersCount: activeOrders.data.length,
      latestOrder: activeOrders.data[0] || null,
      notifications: notifications.data.map(n => ({ id: n.id, title: n.title })),
    });
  } catch (err) {
    res.status(500).json({ error: 'Failed to assemble mobile dashboard feed' });
  }
});

app.listen(4000, () => console.log('🚀 Edge API Gateway / BFF running on port 4000'));

Architectural Guidelines for API Gateways

  • Keep Gateway Business Logic Free: Do not leak domain logic or direct database access into the main API Gateway. Move aggregation logic into separate BFF microservices.
  • Implement Circuit Breakers: Wrap downstream microservice calls with libraries like opossum so failing downstream services return cached fallbacks instead of crashing gateway threads.
  • Offload Heavy Analytics: Avoid aggregating large datasets in Node.js event loops synchronously. Use GraphQL or specialized read-replicas for telemetry-heavy payloads.
Advertisement
Advertisement