- Published on
API Gateway Patterns — Rate Limiting, Auth, and Request Transformation at the Edge
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
An API gateway sits between clients and your services, handling cross-cutting concerns: authentication, rate limiting, request transformation, routing. Built well, a gateway scales your infrastructure. Built poorly, it becomes a bottleneck and single point of failure. We'll design gateways for resilience, understand what belongs at the edge vs in services, and compare commercial options.
- Gateway vs Service Mesh Responsibilities
- JWT Validation at Gateway
- Rate Limiting at Gateway
- Request Aggregation (BFF Pattern)
- API Versioning at Gateway
- Gateway-Level Circuit Breaking
- Kong vs AWS API Gateway vs Custom Express Gateway
- Checklist
- Conclusion
Gateway vs Service Mesh Responsibilities
Gateway handles:
- Authentication (JWT validation, OAuth flow)
- Rate limiting (per user, per IP)
- Request aggregation (BFF—Backend For Frontend)
- API versioning
- Public API enforcement
Service mesh (Istio, Linkerd) handles:
- Inter-service communication routing
- Circuit breaking for service-to-service calls
- Service-to-service authentication (mTLS)
- Metrics collection
// Gateway-level responsibilities
class APIGateway {
async handleRequest(req: Request): Promise<Response> {
// 1. Rate limit by user/IP
if (await this.rateLimiter.isRateLimited(req)) {
return new Response('Too Many Requests', { status: 429 });
}
// 2. Authenticate JWT
const user = await this.validateJWT(req);
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
// 3. Check authorization
if (!this.canAccess(user, req.path)) {
return new Response('Forbidden', { status: 403 });
}
// 4. Transform request (API versioning, aggregation)
const transformed = await this.transformRequest(req, user);
// 5. Route to service
const response = await this.routeToService(transformed);
// 6. Transform response
return this.transformResponse(response, user);
}
private async validateJWT(req: Request): Promise<any | null> {
const authHeader = req.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return null;
}
const token = authHeader.slice(7);
try {
return await this.jwt.verify(token, this.publicKey);
} catch {
return null;
}
}
private canAccess(user: any, path: string): boolean {
// Check user permissions
const resource = path.split('/')[1]; // /users/123 -> 'users'
return user.roles.includes(`read:${resource}`);
}
private async transformRequest(req: Request, user: any): Promise<Request> {
// Add user context header
const headers = new Headers(req.headers);
headers.set('x-user-id', user.sub);
headers.set('x-user-roles', user.roles.join(','));
return new Request(req, { headers });
}
private async routeToService(req: Request): Promise<Response> {
const path = new URL(req.url).pathname;
const service = this.getServiceForPath(path);
return await fetch(`${service}${path}`, req);
}
private transformResponse(res: Response, user: any): Response {
// Remove internal headers before returning to client
const headers = new Headers(res.headers);
headers.delete('x-internal-trace-id');
return new Response(res.body, { status: res.status, headers });
}
private getServiceForPath(path: string): string {
const routes: Record<string, string> = {
users: 'http://user-service:3000',
products: 'http://product-service:3000',
orders: 'http://order-service:3000',
};
const resource = path.split('/')[1];
return routes[resource] || 'http://default-service:3000';
}
}
JWT Validation at Gateway
Validate JWTs at the gateway to reduce per-service overhead.
class GatewayJWTValidator {
private cachedPublicKeys = new Map<string, string>();
private keyRefreshInterval = 3600000; // 1 hour
constructor(private jwksUrl: string) {
this.refreshPublicKeys();
setInterval(() => this.refreshPublicKeys(), this.keyRefreshInterval);
}
async validateToken(token: string): Promise<any> {
const decoded = jwt.decode(token, { complete: true });
if (!decoded) {
throw new Error('Invalid token format');
}
const { header, payload } = decoded;
// Get public key for this token's key ID
const publicKey = await this.getPublicKey(header.kid);
try {
// Verify signature
jwt.verify(token, publicKey, {
algorithms: [header.alg],
issuer: 'https://auth.example.com',
audience: 'api.example.com',
});
return payload;
} catch (error) {
throw new Error(`JWT verification failed: ${error}`);
}
}
private async getPublicKey(kid: string): Promise<string> {
// Check cache first
if (this.cachedPublicKeys.has(kid)) {
return this.cachedPublicKeys.get(kid)!;
}
// Fetch JWKS
const response = await fetch(this.jwksUrl);
const jwks = await response.json();
const key = jwks.keys.find((k: any) => k.kid === kid);
if (!key) {
throw new Error(`Key ${kid} not found in JWKS`);
}
const publicKey = key.x5c[0];
this.cachedPublicKeys.set(kid, publicKey);
return publicKey;
}
private async refreshPublicKeys(): Promise<void> {
try {
const response = await fetch(this.jwksUrl);
const jwks = await response.json();
this.cachedPublicKeys.clear();
for (const key of jwks.keys) {
this.cachedPublicKeys.set(key.kid, key.x5c[0]);
}
} catch (error) {
console.error('Failed to refresh public keys:', error);
}
}
}
Rate Limiting at Gateway
Rate limit before requests reach your services.
class GatewayRateLimiter {
private redis: any;
private limits: Record<string, { rps: number; burst: number }> = {
'free-tier': { rps: 10, burst: 20 },
'paid-tier': { rps: 100, burst: 200 },
'enterprise-tier': { rps: 1000, burst: 2000 },
};
async isAllowed(userId: string, tier: string): Promise<boolean> {
const limit = this.limits[tier];
if (!limit) {
throw new Error(`Unknown tier: ${tier}`);
}
const key = `rate-limit:${userId}`;
// Use Lua script for atomic increment + check
const script = `
local current = redis.call("incr", KEYS[1])
if current == 1 then
redis.call("expire", KEYS[1], 1)
end
return current <= ARGV[1] and "OK" or "RATE_LIMIT_EXCEEDED"
`;
const result = await this.redis.eval(script, 1, key, limit.rps);
return result === 'OK';
}
async limitByIP(ip: string): Promise<boolean> {
// Separate limit for unauthenticated requests
const key = `rate-limit:ip:${ip}`;
const script = `
local current = redis.call("incr", KEYS[1])
if current == 1 then
redis.call("expire", KEYS[1], 60)
end
return current <= ARGV[1] and "OK" or "RATE_LIMIT_EXCEEDED"
`;
const result = await this.redis.eval(script, 1, key, 1000);
return result === 'OK';
}
async checkTokenBucket(userId: string, tier: string, tokens = 1): Promise<boolean> {
const limit = this.limits[tier];
const key = `token-bucket:${userId}`;
const script = `
local current = redis.call("get", KEYS[1])
if not current then
current = ARGV[1]
end
if tonumber(current) >= tonumber(ARGV[2]) then
return "RATE_LIMIT_EXCEEDED"
end
redis.call("incrby", KEYS[1], ARGV[2])
redis.call("expire", KEYS[1], 60)
return "OK"
`;
const result = await this.redis.eval(
script,
1,
key,
limit.burst,
tokens
);
return result === 'OK';
}
}
Request Aggregation (BFF Pattern)
For complex clients, aggregate multiple downstream requests in the gateway.
class BFFGateway {
async getUserDashboard(userId: string): Promise<DashboardResponse> {
// Client requests dashboard; gateway aggregates data from multiple services
const [user, orders, recommendations] = await Promise.all([
this.userService.getUser(userId),
this.orderService.getOrders(userId),
this.recommendationService.getRecommendations(userId),
]);
return {
user: {
id: user.id,
name: user.name,
email: user.email,
},
recentOrders: orders.slice(0, 5).map(o => ({
id: o.id,
total: o.total,
date: o.createdAt,
})),
recommendations: recommendations.map(r => ({
productId: r.productId,
title: r.title,
score: r.score,
})),
};
}
async getProductWithReviews(productId: string): Promise<ProductResponse> {
// Fetch product and reviews concurrently
const [product, reviews, seller] = await Promise.all([
this.productService.getProduct(productId),
this.reviewService.getReviews(productId),
this.sellerService.getSeller(productId),
]);
return {
...product,
reviews: reviews.map(r => ({
author: r.authorId,
rating: r.rating,
text: r.text,
})),
seller: {
name: seller.name,
rating: seller.rating,
},
};
}
private userService = {
getUser: async (userId: string) => ({}),
};
private orderService = {
getOrders: async (userId: string) => [],
};
private recommendationService = {
getRecommendations: async (userId: string) => [],
};
private productService = {
getProduct: async (productId: string) => ({}),
};
private reviewService = {
getReviews: async (productId: string) => [],
};
private sellerService = {
getSeller: async (productId: string) => ({}),
};
}
interface DashboardResponse {
user: any;
recentOrders: any[];
recommendations: any[];
}
interface ProductResponse {
reviews: any[];
seller: any;
}
API Versioning at Gateway
Version your API without requiring service changes.
class APIVersioningGateway {
async handleRequest(req: Request): Promise<Response> {
const url = new URL(req.url);
const version = this.extractVersion(url.pathname);
let path = url.pathname;
// Transform request based on version
if (version === 'v1') {
const body = await req.json();
const transformedBody = this.transformV1RequestToV2(body);
req = new Request(req, {
body: JSON.stringify(transformedBody),
method: 'POST',
});
path = path.replace('/v1/', '/v2/');
}
// Route to service
const response = await fetch(`http://api-service:3000${path}`, req);
// Transform response based on version
if (version === 'v1') {
const body = await response.json();
const transformedBody = this.transformV2ResponseToV1(body);
return Response.json(transformedBody);
}
return response;
}
private extractVersion(pathname: string): string {
const match = pathname.match(/\/(v\d+)\//);
return match ? match[1] : 'v2';
}
private transformV1RequestToV2(v1Body: any): any {
return {
customerId: v1Body.id,
chargeAmount: v1Body.amount,
};
}
private transformV2ResponseToV1(v2Body: any): any {
return {
transaction_id: v2Body.chargeId,
state: v2Body.status === 'completed' ? 'success' : 'pending',
};
}
}
Gateway-Level Circuit Breaking
Protect your gateway and downstream services.
class CircuitBreakerGateway {
private breakers = new Map<string, CircuitBreaker>();
async routeToService(serviceName: string, request: Request): Promise<Response> {
const breaker = this.getOrCreateBreaker(serviceName);
const serviceUrl = this.serviceRegistry.get(serviceName);
return await breaker.execute(async () => {
try {
const response = await fetch(serviceUrl, request);
if (!response.ok && response.status >= 500) {
throw new Error(`Service error: ${response.status}`);
}
return response;
} catch (error) {
console.error(`Request to ${serviceName} failed:`, error);
throw error;
}
});
}
private getOrCreateBreaker(serviceName: string): CircuitBreaker {
if (!this.breakers.has(serviceName)) {
this.breakers.set(
serviceName,
new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 60000,
name: serviceName,
})
);
}
return this.breakers.get(serviceName)!;
}
private serviceRegistry = new Map<string, string>([
['user-service', 'http://user-service:3000'],
['order-service', 'http://order-service:3000'],
]);
}
class CircuitBreaker {
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private failureCount = 0;
private lastFailureTime = 0;
private successCount = 0;
constructor(
private options: {
failureThreshold: number;
resetTimeout: number;
name: string;
}
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.options.resetTimeout) {
this.state = 'HALF_OPEN';
this.successCount = 0;
} else {
throw new Error(`Circuit breaker OPEN for ${this.options.name}`);
}
}
try {
const result = await fn();
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= 2) {
this.state = 'CLOSED';
this.failureCount = 0;
}
}
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.options.failureThreshold) {
this.state = 'OPEN';
}
throw error;
}
}
}
Kong vs AWS API Gateway vs Custom Express Gateway
Kong is open-source with plugin ecosystem. AWS API Gateway is fully managed. Custom Express is simple and owned.
const kongConfig = {
services: [
{
name: 'user-service',
url: 'http://user-service:3000',
routes: [
{
paths: ['/users'],
methods: ['GET', 'POST'],
},
],
},
],
plugins: [
{
name: 'rate-limiting',
config: {
minute: 100,
hour: 10000,
},
},
{
name: 'jwt',
config: {
secret: 'your-secret-key',
},
},
{
name: 'cors',
config: {
origins: ['https://example.com'],
},
},
],
};
// Custom Express gateway
import express from 'express';
class CustomGateway {
private app = express();
setupMiddleware(): void {
// Authentication
this.app.use((req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
try {
(req as any).user = this.jwt.verify(token, this.publicKey);
} catch {
return res.status(401).json({ error: 'Unauthorized' });
}
}
next();
});
// Rate limiting
this.app.use((req, res, next) => {
const userId = (req as any).user?.id || req.ip;
this.rateLimiter.checkLimit(userId).then(allowed => {
if (allowed) {
next();
} else {
res.status(429).json({ error: 'Rate limited' });
}
});
});
// Routes
this.app.use('/users', this.createProxy('http://user-service:3000'));
this.app.use('/orders', this.createProxy('http://order-service:3000'));
}
private createProxy(target: string): any {
return async (req: any, res: any) => {
const response = await fetch(`${target}${req.path}`, {
method: req.method,
headers: req.headers,
body: req.body,
});
return response;
};
}
private jwt: any;
private publicKey = '';
private rateLimiter: any;
}
Checklist
- Validate JWTs at gateway to reduce per-service overhead
- Implement rate limiting per user, per tier, and per IP
- Use circuit breakers to prevent cascading failures
- Aggregate requests for mobile clients (BFF pattern)
- Version APIs at the gateway, not services
- Log all requests with correlation IDs for debugging
- Monitor gateway latency and error rates
- Test gateway failover and resilience
- Use service discovery for dynamic routing
- Cache responses when appropriate
Conclusion
API gateways are force multipliers for distributed systems—they centralize cross-cutting concerns and reduce work per service. Keep them simple and stateless; add a second instance for high availability. Choose between managed (AWS API Gateway) for simplicity or Kong for flexibility. Don't put business logic in your gateway; that's what services are for.