Gateway Routing Pattern
Route requests to multiple backend services through a single entry point that handles cross-cutting concerns.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
The Gateway Routing Pattern places a single entry point in front of multiple backend services. Instead of exposing every service directly to clients, the gateway receives requests and routes them to the appropriate upstream based on path, method, headers, or other rules. It also centralizes cross-cutting concerns such as TLS termination, authentication, rate limiting, and logging.
This pattern is essential for microservices and modular architectures where you want a clean external contract while allowing internal services to evolve independently.
When to Use
Use this pattern when:
- You have multiple backend services that clients must reach through a single address
- You need to enforce TLS, authentication, or rate limiting in one place
- You want to route traffic by URL path, host, or API version without changing clients
- You are migrating from a monolith to microservices and need to hide internal changes
- You need to compose responses from several services or apply protocol translation
Solution
// Simplified gateway route configuration (Express-style)
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
const app = express();
app.use('/users', createProxyMiddleware({
target: 'http://users-service:3001',
changeOrigin: true,
}));
app.use('/orders', createProxyMiddleware({
target: 'http://orders-service:3002',
changeOrigin: true,
}));
app.use('/inventory', createProxyMiddleware({
target: 'http://inventory-service:3003',
changeOrigin: true,
}));
app.listen(3000, () => console.log('Gateway listening on port 3000'));
# Example NGINX location-based routing
server {
listen 443 ssl;
server_name api.example.com;
location /users {
proxy_pass http://users-service;
}
location /orders {
proxy_pass http://orders-service;
}
location /inventory {
proxy_pass http://inventory-service;
}
}
Explanation
The Gateway Routing Pattern works by inserting a reverse proxy or dedicated gateway between clients and services. The gateway maintains a routing table that maps incoming request characteristics to upstream destinations. When a request arrives, the gateway matches it against the table, applies any middleware, and forwards the request. Responses travel back through the gateway, which can transform headers or cache results.
Key responsibilities of the gateway include:
- Routing: match requests to services based on path, host, headers, or version
- Load balancing: distribute requests across healthy upstream instances
- Security: terminate TLS, validate tokens, and enforce rate limits
- Observability: collect metrics and logs for all traffic
Variants
| Variant | Use Case | Trade-off |
|---|---|---|
| API Gateway | Expose public APIs to external clients | Centralized but can become a bottleneck |
| Backend for Frontend | Tailor APIs for web, mobile, or partner clients | Adds a service per client type |
| Edge Gateway | Handle TLS, DDoS, and caching at the network edge | Simplifies origins but adds vendor dependency |
| Internal Gateway | Route traffic inside a cluster with mTLS | Keeps traffic private and secure |
What Works
- Keep the gateway stateless so it can scale horizontally
- Store routing rules in configuration rather than hard-coding them
- Use health checks to avoid routing to failed upstream services
- Offload TLS at the gateway to reduce certificate complexity in services
- Limit gateway logic to cross-cutting concerns; avoid business logic
- Log request IDs and correlation IDs for distributed tracing
Common Mistakes
- Putting business logic in the gateway, making it hard to maintain
- Routing every microservice through a single gateway without scaling it
- Ignoring timeout and retry settings, causing cascading failures
- Forgetting to validate TLS certificates on upstream connections
- Routing based on fragile rules such as query strings that change frequently
Advanced Solutions
Dynamic routing with service discovery
Integrate gateway routing with service discovery for automatic upstream updates:
import { ServiceRegistry } from './service-registry';
import { createProxyMiddleware } from 'http-proxy-middleware';
class DynamicGateway {
private registry: ServiceRegistry;
private app: express.Application;
constructor(registry: ServiceRegistry) {
this.registry = registry;
this.app = express();
this.setupRoutes();
}
async setupRoutes() {
const services = await this.registry.getAllServices();
services.forEach(service => {
const targets = service.instances.map(
instance => `${instance.host}:${instance.port}`
);
this.app.use(service.path, createProxyMiddleware({
target: `http://${targets[0]}`,
changeOrigin: true,
router: (req) => {
// Load balance across healthy instances
const healthyInstances = service.instances.filter(i => i.healthy);
const selected = healthyInstances[Math.floor(Math.random() * healthyInstances.length)];
return `${selected.host}:${selected.port}`;
},
onProxyReq: (proxyReq, req, res) => {
proxyReq.setHeader('X-Request-ID', req.id);
},
onError: (err, req, res) => {
console.error(`Proxy error: ${err.message}`);
res.status(502).json({ error: 'Bad Gateway' });
}
}));
});
}
listen(port: number) {
this.app.listen(port, () => console.log(`Gateway listening on ${port}`));
}
}
Circuit breaker integration
Add circuit breaker pattern to prevent cascading failures:
import CircuitBreaker from 'opossum';
const options = {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000
};
class CircuitBreakerGateway {
private breakers: Map<string, any>;
constructor() {
this.breakers = new Map();
}
getBreaker(serviceName: string) {
if (!this.breakers.has(serviceName)) {
const breaker = new CircuitBreaker(
async (url: string, options: RequestInit) => {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
options
);
breaker.on('open', () => console.log(`Circuit opened for ${serviceName}`));
breaker.on('halfOpen', () => console.log(`Circuit half-open for ${serviceName}`));
breaker.on('close', () => console.log(`Circuit closed for ${serviceName}`));
this.breakers.set(serviceName, breaker);
}
return this.breakers.get(serviceName);
}
async proxyRequest(serviceName: string, path: string, request: Request) {
const breaker = this.getBreaker(serviceName);
const serviceUrl = `http://${serviceName}:3001${path}`;
return breaker.fire(serviceUrl, {
method: request.method,
headers: request.headers,
body: request.body
});
}
}
Request transformation middleware
Transform requests and responses at the gateway:
class TransformGateway {
private app: express.Application;
constructor() {
this.app = express();
this.setupTransforms();
}
setupTransforms() {
// Transform request headers
this.app.use('/api/v1', (req, res, next) => {
req.headers['x-api-version'] = 'v1';
req.headers['x-request-time'] = new Date().toISOString();
next();
});
// Transform response format
this.app.use('/api/v2', async (req, res, next) => {
const originalJson = res.json;
res.json = function(data) {
const transformed = {
meta: {
version: 'v2',
timestamp: new Date().toISOString()
},
data: data
};
originalJson.call(this, transformed);
};
next();
});
// Protocol translation (REST to gRPC)
this.app.post('/grpc-proxy', async (req, res) => {
const grpcClient = loadGrpcClient('users-service');
const grpcRequest = mapRestToGrpc(req.body);
try {
const grpcResponse = await grpcClient.getUser(grpcRequest);
const restResponse = mapGrpcToRest(grpcResponse);
res.json(restResponse);
} catch (error) {
res.status(500).json({ error: 'gRPC translation failed' });
}
});
}
}
Additional Common Mistakes
-
Creating a single point of failure. The gateway becomes critical infrastructure. Deploy multiple gateway instances behind a load balancer with health checks to ensure high availability.
-
Overloading the gateway with transformation logic. Complex transformations increase latency and make debugging difficult. Move heavy transformation logic to dedicated BFF (Backend for Frontend) services.
Frequently Asked Questions
- What is the difference between Gateway Routing and an API Gateway?
Gateway Routing is the routing capability. An API Gateway usually adds authentication, rate limiting, transformation, and developer portal capabilities on top of routing.
- Should the gateway handle retries?
The gateway may retry safe, idempotent requests, but be careful with retries on POST or other state-changing operations to avoid duplicate work.
- Can I use this pattern with serverless functions?
Yes. Functions can be registered as upstream targets and routed by path or HTTP method, just like container services.
Related Resources
API Gateway Design: Resilience, Routing, and Security
A practical guide to designing API gateways: routing patterns, rate limiting, authentication, circuit breakers, and observability for resilient APIs.
PatternAnti-Corruption Layer: Isolate Legacy with Adapters
How to isolate legacy systems with translation adapters. Covers ACL facade, domain translation, bidirectional mapping, and gradual legacy replacement.
PatternBackend for Frontend (BFF) Pattern
Create dedicated backend services tailored to the specific needs of each frontend client type, aggregating downstream APIs and optimizing data shapes per platform.