API Gateway Patterns: The Front Door to Your Microservices
Master API Gateway patterns for microservices architecture. Learn request routing, authentication, rate limiting, and service mesh integration with TypeScript examples.
// table of contents (17 sections)
A single frontend call often triggers multiple backend services. User profile, recommendations, notifications — each lives in its own microservice. Without an API Gateway, your frontend makes 5+ calls per page load. With one, it makes a single request to a unified entry point that handles routing, authentication, and aggregation.
API Gateways are the unsung heroes of microservices architecture. They centralize cross-cutting concerns so your services can focus on business logic.
The Problem: Frontend-Backend Coupling
Without a gateway, microservices create complexity for clients:
Frontend needs user data:
├── GET /api/users/:id → User Service (port 3001)
├── GET /api/users/:id/orders → Order Service (port 3002)
├── GET /api/users/:id/notifications → Notification Service (port 3003)
└── GET /api/recommendations/:id → Recommendation Service (port 3004)
Result: 4 network calls, 4x latency, 4x failure points
The API Gateway pattern solves this with a single entry point:
Frontend → API Gateway → Parallel calls to services
↑
Single request, unified response
Core Responsibilities
An API Gateway handles cross-cutting concerns that every service needs:
1. Request Routing
Route requests to the appropriate service based on path, headers, or query parameters:
interface Route {
path: string;
service: string;
methods: string[];
rewrite?: (path: string) => string;
}
const routes: Route[] = [
{ path: '/api/users', service: 'user-service', methods: ['GET', 'POST', 'PUT'] },
{ path: '/api/orders', service: 'order-service', methods: ['GET', 'POST'] },
{ path: '/api/notifications', service: 'notification-service', methods: ['GET'] },
{
path: '/api/v1/products',
service: 'product-service',
methods: ['GET'],
rewrite: (path) => path.replace('/api/v1', '/api')
},
];
class ApiGateway {
private serviceRegistry: Map<string, string>;
constructor() {
this.serviceRegistry = new Map([
['user-service', 'http://user-service:3001'],
['order-service', 'http://order-service:3002'],
['notification-service', 'http://notification-service:3003'],
]);
}
async route(req: Request): Promise<Response> {
const url = new URL(req.url);
const route = this.matchRoute(url.pathname, req.method);
if (!route) {
return new Response('Not Found', { status: 404 });
}
const targetUrl = this.serviceRegistry.get(route.service);
const rewrittenPath = route.rewrite?.(url.pathname) ?? url.pathname;
return fetch(`${targetUrl}${rewrittenPath}${url.search}`, {
method: req.method,
headers: req.headers,
body: req.body,
});
}
private matchRoute(path: string, method: string): Route | null {
return routes.find(r =>
path.startsWith(r.path) && r.methods.includes(method)
) ?? null;
}
}
2. Authentication & Authorization
Centralize auth logic so services don’t repeat it:
import { verify } from 'jsonwebtoken';
interface AuthConfig {
jwtSecret: string;
publicPaths: string[];
}
class AuthMiddleware {
constructor(private config: AuthConfig) {}
async handle(req: Request): Promise<{ user: any } | null> {
const url = new URL(req.url);
// Skip auth for public paths
if (this.config.publicPaths.some(p => url.pathname.startsWith(p))) {
return null;
}
const authHeader = req.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
throw new AuthError('Missing authorization header', 401);
}
const token = authHeader.slice(7);
try {
const user = verify(token, this.config.jwtSecret);
// Add user info to headers for downstream services
req.headers.set('X-User-Id', user.id);
req.headers.set('X-User-Roles', user.roles.join(','));
return { user };
} catch (err) {
throw new AuthError('Invalid token', 401);
}
}
}
3. Rate Limiting
Protect your services from abuse with centralized rate limiting:
interface RateLimitConfig {
windowMs: number;
maxRequests: number;
keyGenerator?: (req: Request) => string;
}
class RateLimiter {
private requests: Map<string, number[]> = new Map();
constructor(private config: RateLimitConfig) {}
async check(req: Request): Promise<void> {
const key = this.config.keyGenerator?.(req) ?? this.defaultKeyGenerator(req);
const now = Date.now();
const timestamps = this.requests.get(key) ?? [];
const validTimestamps = timestamps.filter(t => now - t < this.config.windowMs);
if (validTimestamps.length >= this.config.maxRequests) {
const retryAfter = Math.ceil(
(validTimestamps[0] + this.config.windowMs - now) / 1000
);
throw new RateLimitError('Too many requests', 429, retryAfter);
}
validTimestamps.push(now);
this.requests.set(key, validTimestamps);
}
private defaultKeyGenerator(req: Request): string {
const ip = req.headers.get('X-Forwarded-For') ?? 'unknown';
const userId = req.headers.get('X-User-Id') ?? ip;
return `${userId}:${new URL(req.url).pathname}`;
}
}
For production, use Redis-backed rate limiting:
import { Redis } from 'ioredis';
class RedisRateLimiter {
constructor(
private redis: Redis,
private config: RateLimitConfig
) {}
async check(req: Request): Promise<RateLimitInfo> {
const key = `ratelimit:${this.getKey(req)}`;
const now = Date.now();
const windowStart = now - this.config.windowMs;
// Use Redis pipeline for atomic operations
const result = await this.redis
.pipeline()
.zremrangebyscore(key, 0, windowStart)
.zcard(key)
.zadd(key, now, `${now}:${Math.random()}`)
.expire(key, Math.ceil(this.config.windowMs / 1000))
.exec();
const currentCount = result[1][1] as number;
const remaining = Math.max(0, this.config.maxRequests - currentCount - 1);
return {
allowed: currentCount < this.config.maxRequests,
remaining,
resetAt: now + this.config.windowMs,
};
}
}
4. Request Aggregation
Combine multiple service calls into a single response:
interface AggregationSpec {
services: {
name: string;
url: string;
path: string;
method?: string;
body?: any;
}[];
combine: (results: Record<string, any>) => any;
}
class RequestAggregator {
async aggregate(spec: AggregationSpec): Promise<any> {
// Execute all requests in parallel
const promises = spec.services.map(async (service) => {
const response = await fetch(service.url + service.path, {
method: service.method ?? 'GET',
body: service.body ? JSON.stringify(service.body) : undefined,
headers: { 'Content-Type': 'application/json' },
});
return {
name: service.name,
data: await response.json(),
};
});
const results = await Promise.all(promises);
// Convert to object for combine function
const resultMap = results.reduce((acc, r) => {
acc[r.name] = r.data;
return acc;
}, {} as Record<string, any>);
return spec.combine(resultMap);
}
}
// Example: User dashboard aggregation
const dashboardSpec: AggregationSpec = {
services: [
{ name: 'user', url: 'http://user-service', path: '/api/users/123' },
{ name: 'orders', url: 'http://order-service', path: '/api/users/123/orders' },
{ name: 'notifications', url: 'http://notification-service', path: '/api/users/123/notifications' },
],
combine: (results) => ({
user: results.user,
recentOrders: results.orders.slice(0, 5),
unreadNotifications: results.notifications.filter((n: any) => !n.read),
}),
};
Gateway Implementation Options
Build or buy? Here are your options:
Option 1: Off-the-Shelf Gateways
| Gateway | Best For | Key Features |
|---|---|---|
| Kong | Teams wanting GUI management | Plugin ecosystem, declarative config, admin API |
| Envoy | Service mesh integration | Dynamic config, L7 load balancing, observability |
| Traefik | Container/K8s environments | Auto-discovery, Let’s Encrypt, middleware |
| AWS API Gateway | AWS-native deployments | Lambda integration, authorizers, usage plans |
| NGINX Plus | High-performance routing | Caching, rate limiting, active health checks |
Kong Example (declarative config):
# kong.yml
_format_version: "3.0"
services:
- name: user-service
url: http://user-service:3001
routes:
- name: users-route
paths:
- /api/users
plugins:
- name: rate-limiting
config:
minute: 100
policy: local
- name: jwt
config:
secret_is_base64: false
- name: order-service
url: http://order-service:3002
routes:
- name: orders-route
paths:
- /api/orders
Option 2: Custom Gateway
When you need fine-grained control or want to avoid vendor lock-in:
import { Hono } from 'hono';
const gateway = new Hono();
// Global middleware
gateway.use('*', logger());
gateway.use('*', cors());
gateway.use('*', rateLimitMiddleware);
// Health check (no auth required)
gateway.get('/health', (c) => c.json({ status: 'ok' }));
// Auth middleware for protected routes
gateway.use('/api/*', authMiddleware);
// Route to services
gateway.all('/api/users/*', proxy('http://user-service:3001'));
gateway.all('/api/orders/*', proxy('http://order-service:3002'));
gateway.all('/api/notifications/*', proxy('http://notification-service:3003'));
// Aggregated endpoint
gateway.get('/api/dashboard/:userId', async (c) => {
const userId = c.req.param('userId');
const aggregator = new RequestAggregator();
const dashboard = await aggregator.aggregate({
services: [
{ name: 'user', url: 'http://user-service:3001', path: `/api/users/${userId}` },
{ name: 'orders', url: 'http://order-service:3002', path: `/api/users/${userId}/orders` },
],
combine: (results) => ({
user: results.user,
orders: results.orders,
}),
});
return c.json(dashboard);
});
export default gateway;
Advanced Patterns
Circuit Breaker at Gateway Level
Combine with resilient API patterns to fail fast:
import CircuitBreaker from 'opossum';
class GatewayWithCircuitBreaker {
private breakers: Map<string, CircuitBreaker> = new Map();
async proxy(serviceName: string, request: Request): Promise<Response> {
if (!this.breakers.has(serviceName)) {
this.breakers.set(serviceName, this.createBreaker(serviceName));
}
const breaker = this.breakers.get(serviceName)!;
try {
return await breaker.fire(request);
} catch (err) {
if (breaker.opened) {
// Return cached response or fallback
return this.getFallback(serviceName, request);
}
throw err;
}
}
private createBreaker(serviceName: string): CircuitBreaker {
return new CircuitBreaker(
(req: Request) => this.executeProxy(serviceName, req),
{
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000,
}
);
}
private async getFallback(serviceName: string, request: Request): Promise<Response> {
// Return stale cache data or graceful degradation response
return new Response(
JSON.stringify({
error: 'Service temporarily unavailable',
fallback: true
}),
{ status: 503, headers: { 'Content-Type': 'application/json' } }
);
}
}
GraphQL Federation Gateway
For GraphQL services, use federation instead of REST aggregation:
import { ApolloGateway } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';
const gateway = new ApolloGateway({
serviceList: [
{ name: 'users', url: 'http://user-service:3001/graphql' },
{ name: 'orders', url: 'http://order-service:3002/graphql' },
{ name: 'products', url: 'http://product-service:3003/graphql' },
],
});
const server = new ApolloServer({
gateway,
subscriptions: false,
});
// Single GraphQL endpoint handles all services
// Clients query what they need, gateway resolves across services
Response Caching
Cache expensive operations at the edge:
interface CacheEntry {
data: any;
etag: string;
cachedAt: number;
ttl: number;
}
class ResponseCache {
private cache: Map<string, CacheEntry> = new Map();
async getOrFetch(
key: string,
fetcher: () => Promise<any>,
ttl: number = 60000
): Promise<{ data: any; cached: boolean; etag: string }> {
const entry = this.cache.get(key);
const now = Date.now();
if (entry && now - entry.cachedAt < ttl) {
return { data: entry.data, cached: true, etag: entry.etag };
}
const data = await fetcher();
const etag = `"${Buffer.from(JSON.stringify(data)).toString('base64').slice(0, 20)}"`;
this.cache.set(key, { data, etag, cachedAt: now, ttl });
return { data, cached: false, etag };
}
invalidate(pattern: string): void {
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
}
}
Gateway vs Service Mesh
How do gateways relate to service meshes like Istio or Linkerd?
┌─────────────────────────────────────────────────────────────┐
│ Internet │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ API Gateway │
│ • Authentication │
│ • Rate Limiting │
│ • Request Aggregation │
│ • Public API Management │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Service Mesh │
│ • Internal service-to-service communication │
│ • mTLS │
│ • Traffic splitting │
│ • Observability │
└─────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Service │ │ Service │ │ Service │ │ Service │
│ A │ │ B │ │ C │ │ D │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
Key distinction:
- API Gateway: North-south traffic (client → services)
- Service Mesh: East-west traffic (service ↔ service)
Use both together: Gateway handles external concerns, mesh handles internal.
Production Checklist
Before deploying your gateway:
- Health checks —
/healthendpoint for load balancer - Graceful shutdown — Drain connections before exit
- Timeout configuration — Don’t let slow services hang requests
- Circuit breakers — Prevent cascading failures
- Rate limiting — Per-user and global limits
- Observability — Logs, metrics, distributed traces
- Request ID — Correlate requests across services
- Compression — Gzip/Brotli for large responses
- Security headers — CORS, CSP, HSTS
- Blue-green deployment — Zero-downtime gateway updates
// Request ID middleware for tracing
gateway.use('*', async (c, next) => {
const requestId = c.req.header('X-Request-Id') ?? crypto.randomUUID();
c.set('requestId', requestId);
c.header('X-Request-Id', requestId);
await next();
});
When to Skip the Gateway
Not every application needs an API Gateway:
- Monoliths — Single service, no routing needed
- Simple CRUD — Direct database access is simpler
- Serverless — API Gateway (AWS) handles it for you
- Small teams — Added complexity may not be worth it
The gateway pattern shines when you have:
- Multiple services that need unified access
- Cross-cutting concerns (auth, rate limiting, logging)
- Client aggregation needs (mobile apps benefit from fewer round trips)
- Need for API versioning and deprecation
Summary
API Gateways centralize the concerns that every microservice needs:
| Concern | Without Gateway | With Gateway |
|---|---|---|
| Auth | Each service validates tokens | Centralized, user info passed via headers |
| Rate Limiting | Scattered, inconsistent | Unified, Redis-backed |
| Routing | Client knows all URLs | Single entry point |
| Aggregation | Multiple client calls | Single request, parallel service calls |
| Observability | Per-service logging | Centralized metrics and tracing |
Build a custom gateway when you need flexibility. Use Kong, Envoy, or Traefik when you want battle-tested solutions with less maintenance overhead.
Combined with circuit breakers for resilience and workflow orchestration for background jobs, your microservices architecture becomes robust and maintainable.
Alhamdulillah for the patterns that make distributed systems manageable.
You might also like
Workflow Orchestration 2026: Temporal vs Inngest vs Trigger.dev
Compare leading workflow orchestration platforms for modern applications. Learn when to use Temporal, Inngest, or Trigger.dev for background jobs, event-driven architectures, and durable workflows.
Background Job Processing Patterns: A Practical Guide for 2026
Master background job processing for scalable applications. Learn queue patterns, retry strategies, dead letter queues, and best practices for reliable async workflows.
Event Sourcing and CQRS: Building Scalable Event-Driven Systems
Master event sourcing and CQRS patterns for high-scale applications. Learn to store events, build read models, and achieve perfect audit trails with practical Go and TypeScript examples.
More Posts
Web Components 2026: Building Framework-Agnostic UI Libraries
Building Autonomous AI Workflows with LangGraph: A Practical Guide
Building Type-Safe APIs with tRPC in 2026: Full-Stack TypeScript Without Schemas
Database Connection Pooling: Patterns for High-Performance Applications
Prompt Caching: Reduce LLM Costs by 90% with Smart Context Management
Building Resilient APIs: Circuit Breakers, Retries, and Rate Limiting in Production
Enjoyed This Post?
Want to discuss the topic, have questions, or looking to collaborate on something similar? Drop a comment below or reach out directly.
