Patrones de API Gateway para microservicios
Cómo los API gateways gestionan enrutamiento, autenticación, rate limiting y agregación de respuestas, y cómo elegir o construir el adecuado.

Los microservicios crean un problema de enrutamiento. Diez servicios significan diez endpoints distintos, diez verificaciones de autenticación, diez limitadores de velocidad. Los clientes no deberían gestionar esta complejidad. Un API gateway se sitúa entre los clientes y los servicios, ofreciendo un punto de entrada único que se encarga de las preocupaciones transversales: enrutamiento, autenticación, rate limiting y transformación de respuestas.
El patrón gateway viene en varias variantes. Elegir la adecuada depende de si necesitas un enrutamiento simple o una agregación compleja de respuestas.
El gateway proxy inverso
El gateway más simple es un proxy inverso. Enruta las solicitudes al servicio correcto según la ruta de la URL y reenvía la respuesta sin cambios.
# Nginx as a basic API gateway
upstream user-service {
server user-service:3001;
server user-service:3002;
}
upstream order-service {
server order-service:3003;
}
upstream product-service {
server product-service:3004;
}
server {
listen 80;
# Route based on path prefix
location /api/users {
proxy_pass http://user-service;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
}
location /api/orders {
proxy_pass http://order-service;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
}
location /api/products {
proxy_pass http://product-service;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
}
# Health check endpoint
location /health {
return 200 '{"status": "ok"}';
add_header Content-Type application/json;
}
}Esto funciona para arquitecturas simples. Pero no puede agregar respuestas de múltiples servicios, transformar payloads ni ejecutar lógica de autenticación personalizada. Para eso necesitas un gateway a nivel de aplicación.
Gateway a nivel de aplicación
Un gateway personalizado escrito en el lenguaje de tu aplicación maneja el enrutamiento complejo, la agregación de respuestas y las cadenas de middleware.
// src/gateway.ts — Express-based API gateway
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
const app = express();
// Middleware chain: logging → auth → rate limit → proxy
app.use(requestLogger);
app.use(authMiddleware);
app.use(rateLimitMiddleware);
// Service routing
const services: Record<string, string> = {
'/api/users': 'http://user-service:3001',
'/api/orders': 'http://order-service:3003',
'/api/products': 'http://product-service:3004',
'/api/notifications': 'http://notification-service:3005',
};
for (const [path, target] of Object.entries(services)) {
app.use(
path,
createProxyMiddleware({
target,
changeOrigin: true,
pathRewrite: { [`^${path}`]: '' },
onError: (err, req, res) => {
console.error(`Proxy error for ${path}:`, err.message);
res.status(502).json({ error: 'Service unavailable' });
},
})
);
}// Authentication middleware at the gateway level
// Services behind the gateway trust the X-User-Id header
async function authMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Missing authentication token' });
}
try {
const payload = await verifyJwt(token);
req.headers['x-user-id'] = payload.sub;
req.headers['x-user-role'] = payload.role;
next();
} catch {
return res.status(401).json({ error: 'Invalid token' });
}
}Con la autenticación gestionada en el gateway, los servicios individuales omiten por completo la verificación del token. Confían en el header X-User-Id porque solo el gateway puede establecerlo.
Backend for Frontend (BFF)
Distintos clientes necesitan distintas formas de API. Una app móvil necesita payloads ligeros. Un dashboard web necesita datos ricos y agregados. El patrón BFF crea un gateway separado para cada tipo de cliente.
// web-bff/src/routes/dashboard.ts — Web BFF aggregates multiple services
app.get('/api/dashboard', async (req, res) => {
const userId = req.headers['x-user-id'] as string;
// Fetch from multiple services in parallel
const [user, orders, notifications, analytics] = await Promise.all([
userService.getProfile(userId),
orderService.getRecent(userId, { limit: 5 }),
notificationService.getUnread(userId),
analyticsService.getSummary(userId),
]);
// Aggregate into a single response optimized for the web dashboard
res.json({
user: {
name: user.name,
avatar: user.avatarUrl,
memberSince: user.createdAt,
},
recentOrders: orders.map((o) => ({
id: o.id,
total: o.total,
status: o.status,
date: o.createdAt,
})),
unreadNotifications: notifications.count,
analytics: {
totalSpent: analytics.totalSpent,
ordersThisMonth: analytics.monthlyOrders,
},
});
});// mobile-bff/src/routes/dashboard.ts — Mobile BFF returns minimal data
app.get('/api/dashboard', async (req, res) => {
const userId = req.headers['x-user-id'] as string;
// Mobile only needs user name and notification count
const [user, notifications] = await Promise.all([
userService.getProfile(userId),
notificationService.getUnread(userId),
]);
res.json({
name: user.name,
avatar: user.avatarUrl,
unread: notifications.count,
});
});Dos BFFs, los mismos servicios backend. El BFF web hace cuatro llamadas a servicios y devuelve un objeto rico. El BFF móvil hace dos llamadas y devuelve un payload mínimo. Ningún cliente hace múltiples solicitudes a la API.
Rate limiting en el gateway
El gateway es el lugar natural para el rate limiting porque ve todo el tráfico entrante antes de que llegue a cualquier servicio.
// src/middleware/rate-limit.ts
import { RateLimiterRedis } from 'rate-limiter-flexible';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
// Different limits for different endpoint tiers
const limiters = {
default: new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'rl:default',
points: 100, // 100 requests
duration: 60, // per 60 seconds
}),
auth: new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'rl:auth',
points: 5, // 5 attempts
duration: 900, // per 15 minutes
}),
heavy: new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'rl:heavy',
points: 10, // 10 requests
duration: 60, // per 60 seconds
}),
};
function getRateLimiter(path: string): RateLimiterRedis {
if (path.startsWith('/api/auth')) return limiters.auth;
if (path.startsWith('/api/reports')) return limiters.heavy;
return limiters.default;
}
export async function rateLimitMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const limiter = getRateLimiter(req.path);
const key = req.headers['x-user-id'] as string || req.ip;
try {
const result = await limiter.consume(key);
res.set('X-RateLimit-Remaining', String(result.remainingPoints));
res.set('X-RateLimit-Reset', String(
Math.ceil(result.msBeforeNext / 1000)
));
next();
} catch (rateLimitRes: any) {
res.set('Retry-After', String(
Math.ceil(rateLimitRes.msBeforeNext / 1000)
));
res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil(rateLimitRes.msBeforeNext / 1000),
});
}
}Los endpoints de autenticación reciben límites estrictos (5 intentos cada 15 minutos) para prevenir la fuerza bruta. Los endpoints de generación de informes reciben límites moderados (10/min) para proteger consultas costosas. Todo lo demás recibe un límite por defecto generoso.
Transformación de solicitudes y respuestas
Los gateways pueden transformar solicitudes y respuestas para mantener la compatibilidad con versiones anteriores o normalizar entre servicios.
// Version transformation — old clients send v1 format, service expects v2
app.post('/api/v1/orders', async (req, res) => {
// Transform v1 request to v2 format
const v2Body = {
items: req.body.products.map((p: any) => ({
productId: p.id,
quantity: p.qty, // v1 used 'qty', v2 uses 'quantity'
unitPrice: p.price, // v1 used 'price', v2 uses 'unitPrice'
})),
shippingAddress: {
...req.body.address,
country: req.body.address.countryCode, // field rename
},
};
const response = await orderService.create(v2Body);
// Transform v2 response back to v1 format
res.json({
orderId: response.id,
total: response.totalAmount,
status: response.orderStatus,
});
});// Response filtering — remove internal fields before sending to clients
function stripInternalFields(data: Record<string, unknown>): Record<string, unknown> {
const internal = ['internalId', 'tenantId', 'auditLog', 'debugInfo'];
const filtered = { ...data };
for (const field of internal) {
delete filtered[field];
}
return filtered;
}
app.use('/api', (req, res, next) => {
const originalJson = res.json.bind(res);
res.json = (body: any) => {
const cleaned = Array.isArray(body)
? body.map(stripInternalFields)
: stripInternalFields(body);
return originalJson(cleaned);
};
next();
});Health checks y circuit breaking
El gateway no debería enrutar tráfico hacia servicios que no están sanos. Los health checks y los circuit breakers previenen fallos en cascada.
// src/health/service-registry.ts
interface ServiceHealth {
url: string;
healthy: boolean;
lastCheck: Date;
consecutiveFailures: number;
}
const services = new Map<string, ServiceHealth>();
async function checkServiceHealth(name: string, url: string): Promise<void> {
const service = services.get(name) || {
url,
healthy: true,
lastCheck: new Date(),
consecutiveFailures: 0,
};
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
await fetch(`${url}/health`, { signal: controller.signal });
clearTimeout(timeout);
service.healthy = true;
service.consecutiveFailures = 0;
} catch {
service.consecutiveFailures++;
// Mark unhealthy after 3 consecutive failures
if (service.consecutiveFailures >= 3) {
service.healthy = false;
}
}
service.lastCheck = new Date();
services.set(name, service);
}
// Check all services every 10 seconds
setInterval(() => {
for (const [name, svc] of services) {
checkServiceHealth(name, svc.url);
}
}, 10000);Cuando un servicio no está sano, el gateway devuelve un 503 de inmediato en lugar de esperar un timeout. Los clientes reciben un error rápido y pueden reintentar o mostrar una experiencia degradada.
Conclusiones clave
- Empieza con un proxy inverso — Nginx maneja el enrutamiento simple basado en rutas sin código personalizado
- Usa un gateway de aplicación para la agregación — cuando los clientes necesitan datos de múltiples servicios en una sola llamada
- El patrón BFF sirve a distintos clientes — móvil y web obtienen formas de API optimizadas desde su propio gateway
- Centraliza la autenticación y el rate limiting — gestiónalo una vez en el gateway, no en cada servicio
- Transforma para la compatibilidad con versiones anteriores — el gateway puede traducir entre versiones de la API sin cambiar los servicios
- Haz health checks a los servicios backend — deja de enrutar hacia servicios no sanos antes de que los clientes experimenten errores


