An API gateway centralizes cross-cutting concerns — authentication, rate limiting, routing, circuit breaking — so individual services don’t implement them separately. Claude Code generates gateway middleware, Kong plugin configurations, and the patterns that make gateways maintainable without becoming a bottleneck.
Express API Gateway Middleware
Build an API gateway in Express.
Features: JWT auth, per-user rate limiting (100 req/min),
request routing to microservices, circuit breaker to handle downstream failures.
JWT Authentication Middleware
// middleware/auth.ts
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
export interface AuthenticatedRequest extends Request {
user?: {
userId: string;
role: string;
scopes: string[];
};
}
export function authenticate(req: AuthenticatedRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing authorization header' });
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, process.env.JWT_PUBLIC_KEY!, {
algorithms: ['RS256'], // Asymmetric — public key validation only
}) as any;
req.user = {
userId: payload.sub,
role: payload.role,
scopes: payload.scopes ?? [],
};
next();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
export function requireScope(scope: string) {
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
if (!req.user?.scopes.includes(scope)) {
return res.status(403).json({ error: `Missing required scope: ${scope}` });
}
next();
};
}
Rate Limiting
// middleware/rateLimiter.ts
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
// Sliding window rate limiter using Redis sorted sets
export function rateLimit(options: {
windowMs: number;
max: number;
keyGenerator: (req: Request) => string;
}) {
return async (req: Request, res: Response, next: NextFunction) => {
const key = `rate-limit:${options.keyGenerator(req)}`;
const now = Date.now();
const windowStart = now - options.windowMs;
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(key, 0, windowStart); // Remove old requests
pipeline.zadd(key, now, `${now}-${Math.random()}`); // Record this request
pipeline.zcard(key); // Count requests in window
pipeline.expire(key, Math.ceil(options.windowMs / 1000)); // Auto-cleanup
const results = await pipeline.exec();
const requestCount = results?.[2]?.[1] as number;
res.setHeader('X-RateLimit-Limit', options.max);
res.setHeader('X-RateLimit-Remaining', Math.max(0, options.max - requestCount));
res.setHeader('X-RateLimit-Reset', Math.ceil((now + options.windowMs) / 1000));
if (requestCount > options.max) {
res.setHeader('Retry-After', Math.ceil(options.windowMs / 1000));
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil(options.windowMs / 1000),
});
}
next();
};
}
// Per-user rate limiting
export const userRateLimit = rateLimit({
windowMs: 60 * 1000, // 1 minute window
max: 100,
keyGenerator: (req) => (req as AuthenticatedRequest).user?.userId ?? req.ip,
});
// Stricter rate limit for expensive endpoints
export const heavyEndpointLimit = rateLimit({
windowMs: 60 * 1000,
max: 10,
keyGenerator: (req) => (req as AuthenticatedRequest).user?.userId ?? req.ip,
});
Circuit Breaker
// middleware/circuitBreaker.ts
enum CircuitState { CLOSED, OPEN, HALF_OPEN }
interface CircuitBreakerOptions {
failureThreshold: number; // Open circuit after N failures
resetTimeout: number; // Try again after N ms
successThreshold: number; // Close circuit after N successes in HALF_OPEN
}
export class CircuitBreaker {
private state = CircuitState.CLOSED;
private failureCount = 0;
private successCount = 0;
private lastFailureTime = 0;
constructor(private options: CircuitBreakerOptions) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime > this.options.resetTimeout) {
this.state = CircuitState.HALF_OPEN;
this.successCount = 0;
} else {
throw new Error('Circuit breaker is OPEN — service unavailable');
}
}
try {
const result = await fn();
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.options.successThreshold) {
this.state = CircuitState.CLOSED;
this.failureCount = 0;
}
} else {
this.failureCount = 0; // Reset on success in CLOSED state
}
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.options.failureThreshold) {
this.state = CircuitState.OPEN;
}
throw error;
}
}
}
// Service proxies with circuit breakers
const serviceBreakers = {
userService: new CircuitBreaker({ failureThreshold: 5, resetTimeout: 30000, successThreshold: 2 }),
orderService: new CircuitBreaker({ failureThreshold: 5, resetTimeout: 30000, successThreshold: 2 }),
};
export async function proxyToService(serviceUrl: string, service: keyof typeof serviceBreakers, req: Request, res: Response) {
try {
const response = await serviceBreakers[service].call(async () => {
const result = await fetch(`${serviceUrl}${req.path}`, {
method: req.method,
headers: {
...req.headers as any,
host: new URL(serviceUrl).host,
'x-user-id': (req as AuthenticatedRequest).user?.userId ?? '',
'x-correlation-id': req.headers['x-correlation-id'] ?? crypto.randomUUID(),
},
body: ['POST', 'PUT', 'PATCH'].includes(req.method) ? JSON.stringify(req.body) : undefined,
signal: AbortSignal.timeout(5000), // 5s timeout
});
if (!result.ok) throw new Error(`Service returned ${result.status}`);
return result;
});
const data = await response.json();
res.status(response.status).json(data);
} catch (error: any) {
if (error.message.includes('Circuit breaker is OPEN')) {
res.status(503).json({ error: 'Service temporarily unavailable', retryAfter: 30 });
} else {
res.status(502).json({ error: 'Upstream service error' });
}
}
}
Route Configuration
// gateway.ts — assemble all middleware
import express from 'express';
import { authenticate, requireScope } from './middleware/auth';
import { userRateLimit, heavyEndpointLimit } from './middleware/rateLimiter';
import { proxyToService } from './middleware/circuitBreaker';
const app = express();
app.use(express.json());
// All routes require authentication
app.use(authenticate);
// User service routes
app.use('/api/v1/users', userRateLimit, (req, res) =>
proxyToService(process.env.USER_SERVICE_URL!, 'userService', req, res)
);
// Order service routes — with scope requirement
app.use('/api/v1/orders', userRateLimit,
requireScope('orders:read'),
(req, res) => proxyToService(process.env.ORDER_SERVICE_URL!, 'orderService', req, res)
);
// Admin routes — heavy rate limit + admin scope
app.use('/api/v1/admin',
heavyEndpointLimit,
requireScope('admin'),
(req, res) => proxyToService(process.env.ADMIN_SERVICE_URL!, 'userService', req, res)
);
app.listen(8080, () => console.log('Gateway on :8080'));
Kong Configuration
We're using Kong as our API gateway. Configure rate limiting,
JWT authentication, and request transformation plugins for our API.
# kong.yaml — declarative configuration
_format_version: "3.0"
services:
- name: user-service
url: http://user-service:3000
routes:
- name: user-routes
paths: [/api/v1/users]
strip_path: false
plugins:
- name: jwt
config:
secret_is_base64: false
key_claim_name: iss
- name: rate-limiting
config:
minute: 100
policy: redis
redis_host: redis
redis_port: 6379
- name: request-transformer
config:
add:
headers:
- "X-Gateway-Version:1.0"
- "X-Request-ID:$(uuid)"
remove:
headers:
- Cookie # Strip cookies before forwarding
- name: public-endpoints
url: http://api-service:3000
routes:
- name: health-route
paths: [/health]
# No auth plugin — public endpoint
For securing the authentication flows these gateways enforce, see the authentication guide. For microservices that sit behind this gateway, see the microservices guide. The Claude Skills 360 bundle includes API gateway skill sets for rate limiting, circuit breakers, and Kong configuration. Start with the free tier to try gateway middleware generation.