API Gateway Patterns
Difficulty: Senior Level | Companies: AWS, Google, Microsoft, Netflix, Uber
Why API Gateway
API Gateway is the single entry point for all client requests. It handles cross-cutting concerns like authentication, rate limiting, and request routing.
โน๏ธ
API Gateway eliminates the need for clients to know internal service addresses. It also provides a place to enforce security policies uniformly.
Gateway Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Clients โ
โ Mobile / Web / Third Party โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโ
โ API Gateway โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Authentication โ โ
โ โ Rate Limiting โ โ
โ โ Request Validation โ โ
โ โ Response Transformation โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โโโโโโโโโโผโโโโโโโ โโโโโโโโโโผโโโโโโโ โโโโโโโโโโผโโโโโโโ
โ User Service โ โ Order Service โ โProduct Serviceโ
โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
Pattern 1: Request/Response Transformation
Transform API shapes between external and internal formats.
// Gateway request transformation
export class RequestTransformer {
transformIncoming(request: IncomingRequest): InternalRequest {
// Flatten nested external format to internal format
return {
userId: request.user.sub,
items: request.body.orderItems.map((item: any) => ({
productId: item.id,
quantity: item.qty,
price: parseFloat(item.unitPrice),
})),
shippingAddress: {
street: request.body.shipping.street,
city: request.body.shipping.city,
state: request.body.shipping.region,
zip: request.body.shipping.postalCode,
country: request.body.shipping.countryCode,
},
metadata: {
source: request.headers['x-client-id'],
correlationId: request.headers['x-correlation-id'],
},
};
}
transformOutgoing(response: InternalResponse): OutgoingResponse {
// Enrich internal response for external consumption
return {
id: response.id,
status: response.status,
items: response.items.map((item: any) => ({
id: item.productId,
name: item.productName,
quantity: item.quantity,
unitPrice: item.price.toString(),
subtotal: (item.quantity * item.price).toString(),
})),
total: response.total.toString(),
shipping: {
address: response.shippingAddress,
estimatedDelivery: response.estimatedDelivery,
},
createdAt: response.createdAt.toISOString(),
};
}
}
// API Gateway middleware
app.use('/api/v1/*', async (req, res, next) => {
try {
// Transform request
const transformer = new RequestTransformer();
const internalRequest = transformer.transformIncoming(req);
// Forward to internal service
const internalResponse = await httpClient.post(
`${INTERNAL_SERVICES_URL}/orders`,
internalRequest,
{
headers: {
'X-User-Id': req.user.sub,
'X-Correlation-Id': req.headers['x-correlation-id'],
},
}
);
// Transform response
const outgoingResponse = transformer.transformOutgoing(internalResponse.data);
res.json(outgoingResponse);
} catch (error) {
next(error);
}
});
Pattern 2: Rate Limiting with Redis
Implement multi-tier rate limiting.
# Redis-based rate limiter
import redis
import time
from dataclasses import dataclass
from typing import Optional
@dataclass
class RateLimitResult:
allowed: bool
remaining: int
retry_after: Optional[float] = None
class MultiTierRateLimiter:
def __init__(self, redis_client: redis.Redis):
self.redis = redis_client
async def check_rate_limit(
self,
identifier: str,
tier: str = 'standard',
) -> RateLimitResult:
limits = {
'free': {'requests': 100, 'window': 60},
'standard': {'requests': 1000, 'window': 60},
'premium': {'requests': 10000, 'window': 60},
}
limit = limits.get(tier, limits['standard'])
key = f"rate_limit:{identifier}:{tier}"
pipe = self.redis.pipeline()
now = time.time()
window_start = now - limit['window']
# Sliding window log algorithm
pipe.zremrangebyscore(key, 0, window_start)
pipe.zadd(key, {str(now): now})
pipe.zcard(key)
pipe.expire(key, limit['window'])
results = pipe.execute()
request_count = results[2]
if request_count > limit['requests']:
# Rate limited - calculate retry after
oldest = self.redis.zrange(key, 0, 0, withscores=True)
retry_after = oldest[0][1] + limit['window'] - now if oldest else limit['window']
return RateLimitResult(
allowed=False,
remaining=0,
retry_after=retry_after,
)
return RateLimitResult(
allowed=True,
remaining=limit['requests'] - request_count,
)
โน๏ธ
Use sliding window algorithms over fixed windows to avoid burst issues at window boundaries.
Pattern 3: JWT Validation at Gateway
Validate tokens once at the gateway, not in every service.
// JWT validation middleware
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 10,
});
async function getKey(header: jwt.JwtHeader): Promise<string> {
return new Promise((resolve, reject) => {
client.getSigningKey(header.kid, (err, key) => {
if (err) return reject(err);
resolve(key.getPublicKey());
});
});
}
export const validateJwt = async (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing authorization token' });
}
const token = authHeader.substring(7);
try {
const decoded = await new Promise<any>((resolve, reject) => {
jwt.verify(
token,
getKey,
{
audience: 'api://gateway',
issuer: 'https://auth.example.com',
algorithms: ['RS256'],
},
(err, payload) => {
if (err) reject(err);
else resolve(payload);
}
);
});
// Attach user info to request for downstream services
req.user = {
sub: decoded.sub,
email: decoded.email,
roles: decoded.roles || [],
permissions: decoded.permissions || [],
};
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
};
Pattern 4: Circuit Breaker for Backend Services
Protect against cascading failures.
// Circuit breaker implementation
enum CircuitState {
CLOSED = 'CLOSED',
OPEN = 'OPEN',
HALF_OPEN = 'HALF_OPEN',
}
class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED;
private failureCount: number = 0;
private lastFailureTime: number = 0;
private successCount: number = 0;
constructor(
private failureThreshold: number = 5,
private resetTimeout: number = 30000,
private halfOpenMaxAttempts: number = 3,
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
this.state = CircuitState.HALF_OPEN;
this.successCount = 0;
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failureCount = 0;
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.halfOpenMaxAttempts) {
this.state = CircuitState.CLOSED;
}
}
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = CircuitState.OPEN;
}
}
}
// Usage in gateway
const orderServiceBreaker = new CircuitBreaker(5, 30000);
app.post('/api/v1/orders', async (req, res) => {
try {
const result = await orderServiceBreaker.execute(() =>
httpClient.post('http://order-service/orders', req.body)
);
res.json(result.data);
} catch (error) {
if (error.message === 'Circuit breaker is OPEN') {
res.status(503).json({
error: 'Service temporarily unavailable',
retryAfter: 30,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
Pattern 5: API Composition at Gateway
Aggregate responses from multiple services.
// Gateway composition endpoint
export class ComposedEndpoints {
constructor(
private userService: UserServiceClient,
private orderService: OrderServiceClient,
private inventoryService: InventoryServiceClient,
) {}
async getUserDashboard(userId: string): Promise<UserDashboard> {
// Fetch all data in parallel
const [user, orders, recommendations] = await Promise.all([
this.userService.getUser(userId),
this.orderService.getUserOrders(userId, { limit: 10 }),
this.inventoryService.getRecommendations(userId),
]);
// Compose response
return {
user: {
id: user.id,
name: user.name,
email: user.email,
},
recentOrders: orders.map(order => ({
id: order.id,
status: order.status,
total: order.total,
itemCount: order.items.length,
})),
recommendations: recommendations.slice(0, 5),
stats: {
totalOrders: orders.length,
totalSpent: orders.reduce((sum, o) => sum + o.total, 0),
},
};
}
}
โ ๏ธ
Gateway composition adds latency (slowest service determines response time). Use it for BFF (Backend for Frontend) patterns where clients need aggregated data.
Gateway Pattern Summary
| Pattern | Use Case | Complexity |
|---|---|---|
| Request Transformation | API versioning | Low |
| Rate Limiting | Abuse prevention | Medium |
| JWT Validation | Centralized auth | Medium |
| Circuit Breaker | Resilience | Medium |
| API Composition | BFF pattern | High |
Follow-Up Questions
- How do you handle API versioning in a gateway without breaking existing clients?
- What strategies would you use to deploy gateway changes without downtime?
- How do you implement request/response logging for debugging without impacting performance?