Desde 02-02 hablamos de un API Gateway en el puerto 8080 "delante" de los servicios: es la pieza que hace posible el strangler fig (la web sigue llamando a una sola dirección mientras las rutas se van moviendo del monolito a los servicios) y la única puerta que verán la web, la app móvil y los socios externos. En 03-03 dejamos, además, dos preguntas abiertas: dónde vive el GraphQL que agrega Pedidos y Catálogo para la app móvil, y por qué hablamos de un backend for frontend. Esta lección responde a ambas.
Veremos qué problemas resuelve un gateway (un punto de entrada, no exponer los puertos 300x, y las responsabilidades transversales: enrutamiento, autenticación delegada, rate limiting, CORS, terminación TLS, agregación, logging), qué no debe hacer, la tabla de rutas del gateway de TechCorp y su papel en el strangler fig, las opciones de implementación, un gateway mínimo pero completo en Express con http-proxy-middleware explicado paso a paso, la configuración declarativa equivalente en Traefik, el patrón Backend for Frontend con un BFF móvil que compone GET /pedidos/{id} + GET /productos?ids=, y los riesgos del gateway. Cómo encuentra el gateway a los servicios (descubrimiento y balanceo) es de 03-05, la autenticación en detalle de 07-01 y el despliegue del gateway del módulo 5.
Contenido
- El problema: muchos servicios, un solo cliente
- Responsabilidades transversales del gateway
- Lo que un gateway NO debe hacer
- Las rutas del gateway de TechCorp y el strangler fig
- Opciones de implementación
- Un gateway mínimo en Express con
http-proxy-middleware - La misma configuración en Traefik (YAML declarativo)
- El patrón Backend for Frontend
- Un BFF móvil que compone pedido y productos
- Riesgos del gateway
- El problema: muchos servicios, un solo cliente
Sin gateway, la web de TechCorp tendría que saber que los productos están en servicio-catalogo:3001, los pedidos en servicio-pedidos:3002 y los clientes en servicio-clientes:3004. Eso trae cinco problemas inmediatos:
- Acoplamiento del cliente a la topología. Cada vez que un servicio cambia de host, se divide o se fusiona, hay que redesplegar la web y publicar una versión nueva de la app móvil (y esperar a que los usuarios la actualicen).
- Superficie de ataque. Exponer seis puertos a Internet es exponer seis superficies que autenticar, parchear y vigilar. Inventario y Pagos, además, no deben ser accesibles desde fuera bajo ningún concepto.
- Duplicación de lo transversal. Autenticación, CORS, límites de peticiones, TLS, registro de accesos: o lo hace cada servicio (seis veces, con seis versiones ligeramente distintas) o lo hace alguien delante de todos.
- Muchos viajes de red. Una pantalla que necesita datos de tres servicios hace tres peticiones desde el móvil, cada una con su latencia y su handshake.
- El strangler fig es imposible. Si la web habla directamente con el monolito, mover
/productosal nuevo servicio requiere cambiar la web. Con un gateway, se cambia una regla de enrutamiento y nadie más se entera.
Un API Gateway es un servidor que recibe todas las peticiones externas y las reenvía al servicio interno adecuado, aplicando por el camino las políticas comunes. Es un reverse proxy con criterio.
flowchart LR
W[Web] --> G
M[App móvil] --> G
S[Socios / ERP] --> G
G[API Gateway :8080]
G -- "/api/productos/*" --> C[servicio-catalogo:3001]
G -- "/api/pedidos/*" --> P[servicio-pedidos:3002]
G -- "/api/clientes/*" --> K[servicio-clientes:3004]
G -- "resto (aún)" --> MO[monolito techcorp-shop:3000]
I[servicio-inventario:3006]:::interno
PA[servicio-pagos:3003]:::interno
N[servicio-notificaciones:3005]:::interno
classDef interno stroke-dasharray: 5 5
Los servicios con línea discontinua no tienen ruta en el gateway: solo hablan por eventos (03-02) o reciben llamadas internas.
- Responsabilidades transversales del gateway
| Responsabilidad | Qué hace el gateway | Por qué aquí y no en cada servicio |
|---|---|---|
| Enrutamiento | Mapea rutas públicas a servicios internos (/api/pedidos/* → servicio-pedidos:3002), reescribiendo el prefijo |
Es su razón de ser; permite el strangler fig |
| Autenticación delegada | Valida el JWT (firma, caducidad, emisor Keycloak) y rechaza con 401 antes de tocar ningún servicio; propaga la identidad en cabeceras o el propio token |
Un solo lugar donde estar al día de claves y algoritmos; los servicios reciben peticiones ya autenticadas (aunque en 07-01 veremos que también deben verificar) |
| Rate limiting | Limita peticiones por IP, por cliente o por token (429 + Retry-After) |
Protege a todos los servicios de golpe; un servicio saturado no puede protegerse a sí mismo |
| CORS | Responde a las peticiones OPTIONS de preflight y añade Access-Control-Allow-* |
El navegador solo ve un origen (el gateway); configurar CORS en seis servicios es garantía de inconsistencia |
| Terminación TLS | Recibe HTTPS del exterior y habla HTTP (o mTLS con el mesh de 05-05) hacia dentro | Un solo certificado que renovar; los servicios no gestionan claves privadas |
| Agregación | Compone varias respuestas internas en una (con matices: apartado 8) | Ahorra viajes al cliente móvil |
| Logging y métricas de acceso | Registra método, ruta, código, latencia y X-Request-Id de cada petición |
Vista única del tráfico entrante; entrada a 06-01 |
| Correlación | Genera X-Request-Id si no viene y lo propaga |
Es el primer salto: si no lo hace él, nadie más puede |
| Timeouts | Corta peticiones que un servicio no responde en N segundos (504) |
Evita que un servicio lento cuelgue conexiones del cliente |
| Transformación ligera | Reescribir rutas, añadir/quitar cabeceras, comprimir | Adaptación entre lo público y lo interno |
- Lo que un gateway NO debe hacer
En 02-01 formulamos "smart endpoints, dumb pipes": la inteligencia vive en los servicios; las tuberías (broker, gateway) solo mueven mensajes. Aplicado al gateway:
- Nada de lógica de negocio. El gateway no calcula totales, no valida que un pedido tenga líneas, no decide si un cliente puede pedir. Si lo hace, se convierte en un mini-monolito que todos los equipos deben tocar para cualquier cambio, y en el peor cuello de botella organizativo.
- No accede a bases de datos de los servicios. Ni para "una consulta rápida".
- No transforma cargas de negocio (renombrar
precioapricepara un cliente). Eso es un BFF (apartado 8) o una versión de API (03-06). - No orquesta sagas. El flujo de pedido vive en los servicios y en RabbitMQ.
- No sustituye a la seguridad de cada servicio. Que el gateway autentique no exime a Pedidos de comprobar que el pedido es de quien lo pide (07-01: defensa en profundidad).
La prueba del algodón: si para cambiar una regla de negocio hay que redesplegar el gateway, la regla está en el sitio equivocado.
- Las rutas del gateway de TechCorp y el strangler fig
Tabla de rutas en el estado actual del plan (Catálogo, Pedidos y Clientes ya extraídos o en extracción):
| Ruta pública (gateway :8080) | Destino interno | Reescritura | Autenticación | Notas |
|---|---|---|---|---|
GET /api/productos, GET /api/productos/{id} |
servicio-catalogo:3001 |
/api/productos → /productos |
Pública (lectura anónima) | Primera ruta migrada (02-02); caché 30 s permitida |
POST /api/pedidos, GET /api/pedidos, GET /api/pedidos/{id}, POST /api/pedidos/{id}/cancelacion |
servicio-pedidos:3002 |
/api/pedidos → /pedidos |
JWT obligatorio | Requiere Idempotency-Key en POST |
GET /api/clientes/{id}, PUT /api/clientes/{id}/direccion |
servicio-clientes:3004 |
/api/clientes → /clientes |
JWT; solo el propio cliente o admin | |
/api/graphql (móvil) |
bff-movil:3010 |
ninguna | JWT | Apartado 9 |
/api/* (todo lo demás) |
monolito techcorp-shop:3000 |
ninguna | según el monolito | Rutas aún no migradas: se van vaciando |
/health |
El propio gateway | — | Pública | Para el balanceador de delante |
| (sin ruta) | servicio-inventario:3006, servicio-pagos:3003, servicio-notificaciones:3005 |
— | — | No expuestos. Se llega a ellos solo por eventos o desde otros servicios |
El strangler fig se ve en la última fila de /api/*: al principio del proyecto todo iba al monolito. El equipo de Plataforma añadió la regla de /api/productos/* cuando Catálogo estuvo listo; añadirá /api/pedidos/* cuando lo esté Pedidos, y así hasta que la regla del monolito no reciba tráfico y se borre. Cada paso es un cambio de configuración del gateway, reversible en segundos: si el nuevo Catálogo falla, se vuelve a apuntar /api/productos/* al monolito.
Dos convenciones: las rutas públicas llevan prefijo /api/ para distinguirlas de los recursos estáticos de la web, y el gateway quita ese prefijo antes de reenviar, de modo que los servicios exponen /productos, /pedidos, exactamente como los contratos de 03-01, sin saber que hay un gateway delante.
- Opciones de implementación
| Opción | Qué es | Ventajas | Inconvenientes | Encaje en TechCorp |
|---|---|---|---|---|
| Kong | Gateway sobre NGINX/OpenResty con plugins (auth, rate limit, transformaciones) y configuración declarativa o por API | Muy completo, ecosistema de plugins, modo declarativo sin BD | Pieza más a operar; curva media | Buena opción a medio plazo |
| NGINX | Reverse proxy clásico configurado con nginx.conf |
Ubicuo, rapidísimo, estable | Configuración estática, sin descubrimiento nativo; rate limit y auth requieren módulos | Sencillo pero rígido para el strangler fig |
| Traefik | Reverse proxy nativo de contenedores; descubre servicios desde Docker/Kubernetes y se configura con YAML o etiquetas | Integración natural con Docker Compose y Kubernetes (Ingress), middlewares de rate limit, headers, auth | Menos plugins que Kong | Elección de TechCorp para producción |
| Spring Cloud Gateway | Gateway programable en Java/Spring | Muy integrado en el ecosistema Spring | TechCorp no usa Java | Solo mención |
Gateway propio en Node.js (http-proxy-middleware o express-http-proxy) |
Un Express que hace de proxy | Total control, mismo lenguaje que el resto, ideal para aprender y para lógica de composición | Hay que implementar y mantener lo que Kong/Traefik dan hecho; riesgo de meter negocio | Para esta lección y para los BFF |
La estrategia de TechCorp: entender el gateway construyendo uno mínimo en Node.js (apartado 6), operar en producción uno declarativo (Traefik, apartado 7; en Kubernetes hará de Ingress, 05-02), y programar solo los BFF, que sí tienen lógica de composición legítima.
- Un gateway mínimo en Express con
http-proxy-middleware
http-proxy-middleware// gateway/servidor.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const { randomUUID } = require('node:crypto');
const app = express();
// 1. Destinos internos. Vienen de configuración (04-03); los nombres estables se justifican en 03-05.
const DESTINOS = {
catalogo: process.env.CATALOGO_URL ?? 'http://servicio-catalogo:3001',
pedidos: process.env.PEDIDOS_URL ?? 'http://servicio-pedidos:3002',
clientes: process.env.CLIENTES_URL ?? 'http://servicio-clientes:3004',
bffMovil: process.env.BFF_MOVIL_URL ?? 'http://bff-movil:3010',
monolito: process.env.MONOLITO_URL ?? 'http://techcorp-shop:3000'
};
// 2. Correlación: si el cliente no trae X-Request-Id, lo generamos aquí. Todo lo que pase
// después (logs del gateway, cabecera al servicio, respuesta al cliente) lo lleva.
app.use((req, res, next) => {
req.requestId = req.get('X-Request-Id') ?? `req-${randomUUID()}`;
res.set('X-Request-Id', req.requestId);
next();
});
// 3. CORS: solo los orígenes de TechCorp. El navegador hace OPTIONS (preflight) y este
// middleware responde; los servicios internos no saben nada de CORS.
app.use(cors({
origin: ['https://shop.techcorp.example', 'https://admin.techcorp.example'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'Idempotency-Key', 'If-Match', 'X-Request-Id'],
exposedHeaders: ['Location', 'ETag', 'X-Request-Id', 'Retry-After'],
maxAge: 600
}));
// 4. Rate limiting: 300 peticiones por minuto por IP en toda la API. Con varias réplicas del
// gateway haría falta un almacén compartido (Redis); en memoria basta para aprender.
app.use('/api', rateLimit({
windowMs: 60_000,
limit: 300,
standardHeaders: 'draft-7', // añade RateLimit-* y Retry-After
legacyHeaders: false,
message: { type: 'about:blank', title: 'Demasiadas peticiones', status: 429, codigo: 'LIMITE_PETICIONES' }
}));
// 5. Log de acceso mínimo (06-01 lo sustituirá por logs estructurados)
app.use((req, res, next) => {
const inicio = Date.now();
res.on('finish', () => {
console.log(JSON.stringify({ requestId: req.requestId, metodo: req.method, ruta: req.originalUrl, estado: res.statusCode, ms: Date.now() - inicio }));
});
next();
});
// 6. Autenticación delegada (esqueleto). La validación real del JWT de Keycloak se ve en 07-01;
// aquí solo comprobamos que la cabecera existe para las rutas protegidas.
function requiereToken(req, res, next) {
const auth = req.get('Authorization') ?? '';
if (!auth.startsWith('Bearer ')) {
return res.status(401).type('application/problem+json').json({ type: 'about:blank', title: 'No autenticado', status: 401, codigo: 'NO_AUTENTICADO' });
}
next();
}
// 7. Fábrica de proxies: misma configuración para todos, cambia destino y reescritura
function proxyHacia(destino, { quitarPrefijo }) {
return createProxyMiddleware({
target: destino,
changeOrigin: true, // pone el Host del destino en la petición reenviada
pathRewrite: quitarPrefijo ? { [`^${quitarPrefijo}`]: '' } : undefined, // /api/pedidos/x → /pedidos/x
proxyTimeout: 5_000, // si el servicio no responde en 5 s → 504 al cliente
timeout: 10_000, // tiempo máximo de la conexión entrante
on: {
proxyReq: (proxyReq, req) => {
proxyReq.setHeader('X-Request-Id', req.requestId); // propagar correlación
proxyReq.setHeader('X-Forwarded-Prefix', quitarPrefijo ?? ''); // el servicio puede construir Location absolutos si quiere
},
error: (err, req, res) => {
// El servicio no está o cerró la conexión: 503 en formato RFC 7807, sin filtrar detalles
if (!res.headersSent) {
res.status(503).type('application/problem+json').json({
type: 'about:blank', title: 'Servicio no disponible', status: 503,
codigo: 'DEPENDENCIA_NO_DISPONIBLE', detail: `Reintente más tarde (ref ${req.requestId})`
});
}
}
}
});
}
// 8. Tabla de rutas. El ORDEN importa: Express evalúa de arriba abajo, y la última es el comodín al monolito.
app.use('/api/productos', proxyHacia(DESTINOS.catalogo, { quitarPrefijo: '/api' }));
app.use('/api/pedidos', requiereToken, proxyHacia(DESTINOS.pedidos, { quitarPrefijo: '/api' }));
app.use('/api/clientes', requiereToken, proxyHacia(DESTINOS.clientes, { quitarPrefijo: '/api' }));
app.use('/api/graphql', requiereToken, proxyHacia(DESTINOS.bffMovil, { quitarPrefijo: '/api' }));
app.use('/api', proxyHacia(DESTINOS.monolito, { quitarPrefijo: null })); // strangler fig: lo no migrado
// 9. Salud del propio gateway (03-05 explica liveness/readiness)
app.get('/health', (_req, res) => res.json({ estado: 'ok' }));
app.listen(8080, () => console.log('API Gateway escuchando en 8080'));Explicación de las decisiones:
- No hay
express.json(). El gateway no necesita parsear los cuerpos: los reenvía tal cual como stream. Parsearlos costaría CPU y rompería la transmisión de cuerpos grandes. Cuando en un proyecto real se añade unexpress.json()global "por costumbre",http-proxy-middlewaredeja de reenviar el cuerpo y todos losPOSTllegan vacíos: es el error clásico. pathRewritees la traducción entre el contrato público (/api/pedidos) y el interno (/pedidos). Los servicios no saben que hay prefijo.proxyTimeout: 5000es la buena práctica mínima de la que hablamos en cada lección; sin él, un Catálogo colgado retiene conexiones del gateway hasta agotarlas. Los reintentos y el circuit breaker son de 06-03.- El orden de las rutas implementa el strangler fig: lo específico primero, el comodín al monolito al final. Migrar una ruta es añadir una línea encima del comodín.
requiereTokenes un esqueleto: en 07-01 verificará firma, caducidad y audience del JWT de Keycloak y pasará la identidad al servicio.- El
errordel proxy responde en el mismo formato RFC 7807 de 03-01, con elrequestIdpara que soporte pueda buscarlo en los logs.
Prueba manual del pedido de siempre a través del gateway (con Pedidos escuchando en 3002):
curl -i -X POST http://localhost:8080/api/pedidos \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f3c9a2e-1b4d-4e8f-9c21-5a6b7c8d9e0f" \
-d '{"clienteId":"c-1024","lineas":[{"productoId":"p-501","cantidad":1},{"productoId":"p-777","cantidad":2}],"direccionEnvio":{"calle":"Gran Vía 12","codigoPostal":"28013","ciudad":"Madrid","pais":"ES"}}'Debería devolver 202 Accepted, Location: /pedidos/ped-88213 (el servicio construye la ruta interna; el gateway podría reescribirla a /api/pedidos/... con un onProxyRes, o el cliente añade el prefijo; TechCorp opta por que el cliente conozca el prefijo /api), y X-Request-Id.
- La misma configuración en Traefik (YAML declarativo)
En producción, mantener un gateway a mano no compensa: Traefik hace lo mismo con YAML. Su modelo tiene tres conceptos: routers (qué peticiones capturo: reglas de host y ruta), middlewares (qué les hago: quitar prefijo, limitar, cabeceras) y services (a dónde las mando, con balanceo). Configuración dinámica equivalente al gateway anterior:
# traefik/dinamica.yml
http:
routers:
productos:
rule: "PathPrefix(`/api/productos`)"
entryPoints: [web]
middlewares: [quitar-api, limite-global, request-id]
service: catalogo
pedidos:
rule: "PathPrefix(`/api/pedidos`)"
entryPoints: [web]
middlewares: [quitar-api, limite-global, request-id, requiere-token]
service: pedidos
clientes:
rule: "PathPrefix(`/api/clientes`)"
entryPoints: [web]
middlewares: [quitar-api, limite-global, request-id, requiere-token]
service: clientes
monolito:
rule: "PathPrefix(`/api`)"
entryPoints: [web]
priority: 1 # la más baja: solo si ninguna anterior encaja (strangler fig)
middlewares: [limite-global, request-id]
service: monolito
middlewares:
quitar-api:
stripPrefix:
prefixes: ["/api"]
limite-global:
rateLimit:
average: 300 # peticiones por minuto (period)
period: 1m
burst: 50
request-id:
headers:
customRequestHeaders:
X-Forwarded-Prefix: "/api" # Traefik ya añade X-Request-Id si se activa el accessLog con ese campo; aquí solo el prefijo
requiere-token:
forwardAuth: # delega la validación en un servicio de auth (07-01)
address: "http://auth-gateway:3020/verificar"
authResponseHeaders: ["X-Usuario-Id", "X-Roles"]
services:
catalogo:
loadBalancer:
servers:
- url: "http://servicio-catalogo:3001"
healthCheck: { path: /health/ready, interval: 10s }
pedidos:
loadBalancer:
servers:
- url: "http://servicio-pedidos:3002"
clientes:
loadBalancer:
servers:
- url: "http://servicio-clientes:3004"
monolito:
loadBalancer:
servers:
- url: "http://techcorp-shop:3000"Y la configuración estática mínima (puertos, y en producción TLS con Let's Encrypt, cuya parte de certificados se ve en 07-02):
# traefik/traefik.yml
entryPoints:
web:
address: ":8080"
providers:
file:
filename: /etc/traefik/dinamica.yml
watch: true # recarga al cambiar el fichero: migrar una ruta no requiere reinicio
accessLog: {}Correspondencia con el código de Express: cada app.use('/api/x', ...) es un router + stripPrefix; express-rate-limit es el middleware rateLimit; requiereToken es forwardAuth; el comodín al monolito es el router con priority: 1; y DESTINOS son los services. Cuando en 05-02 despleguemos en Kubernetes, estos services apuntarán a los Service del clúster y Traefik los descubrirá solo (03-05 explica cómo).
- El patrón Backend for Frontend
Un gateway sirve a todos los clientes por igual. Pero la web de escritorio, la app móvil y el ERP de un socio necesitan cosas distintas: la app quiere respuestas pequeñas y compuestas (una pantalla, una petición); la web tolera varias llamadas y quiere caché; el socio quiere REST estable y documentado. Si el gateway intenta contentar a todos con transformaciones y agregaciones, engorda (riesgo del apartado 10).
El patrón Backend for Frontend (BFF) propone un servicio de fachada por tipo de cliente, propiedad del equipo que hace ese cliente: bff-movil lo mantiene el equipo de la app; bff-web, el de la web. Cada BFF compone y adapta las APIs de los servicios a las necesidades de su front, y solo de él.
| API Gateway | BFF | |
|---|---|---|
| Cuántos | Uno (o uno por zona) | Uno por tipo de cliente |
| Conoce el negocio | No | Sí, el de la presentación: qué necesita cada pantalla |
| Contiene lógica | Transversal (auth, límites) | De composición y adaptación (agregar, filtrar, dar forma) |
| Quién lo mantiene | Plataforma | El equipo del front correspondiente |
| Dónde se sitúa | Delante de todo | Detrás del gateway, delante de los servicios |
| Cambia cuando | Cambia la topología o una política | Cambia una pantalla |
En TechCorp: el gateway enruta /api/graphql al bff-movil:3010 (tabla del apartado 4). La web, por ahora, consume los servicios REST directamente a través del gateway; si sus pantallas se complican, tendrá su bff-web. El ERP de los socios usa la API REST pública sin BFF.
- Un BFF móvil que compone pedido y productos
La pantalla "detalle de pedido" de la app necesita el pedido (Pedidos) y la imagen y disponibilidad actual de cada producto (Catálogo). En 03-03 lo resolvimos con GraphQL + DataLoader, y ese código es exactamente el BFF móvil. Aquí mostramos la alternativa REST, más sencilla, para dejar clara la idea de composición; ambas son válidas y TechCorp probará primero la REST y pasará a GraphQL cuando las pantallas lo pidan.
// bff-movil/rutas/pedidos.js
const express = require('express');
const enrutador = express.Router();
const pedidosApi = require('../clientes/pedidosCliente'); // GET /pedidos/{id} (03-01)
const catalogoApi = require('../clientes/catalogoCliente'); // GET /productos?ids= (03-01)
// GET /movil/pedidos/:id → una sola respuesta con pedido + datos vivos de producto
enrutador.get('/movil/pedidos/:id', async (req, res, next) => {
const ctx = { requestId: req.get('X-Request-Id') };
try {
// 1. Primero el pedido: sin él no sabemos qué productos pedir
const pedido = await pedidosApi.obtenerPedido(req.params.id, ctx);
// 2. Después, UNA llamada en lote a Catálogo (nunca N+1)
const ids = pedido.lineas.map(l => l.productoId);
let productos = [];
try {
productos = await catalogoApi.obtenerProductos(ids, ctx);
} catch (err) {
// 3. Degradación elegante: si Catálogo falla, la pantalla se muestra sin imágenes.
// El BFF decide esto porque conoce la pantalla; el gateway nunca podría.
console.warn('Catálogo no disponible; respuesta sin datos vivos', { requestId: ctx.requestId });
}
const porId = new Map(productos.map(p => [p.id, p]));
// 4. Dar forma para la pantalla: solo lo que la app pinta, con nombres pensados para el front
res.set('Cache-Control', 'no-store').json({
id: pedido.id,
estado: pedido.estado,
estadoLegible: ESTADOS_LEGIBLES[pedido.estado] ?? pedido.estado, // "Confirmado"
total: pedido.total,
puedeCancelar: Boolean(pedido._links?.cancelar), // usa el HATEOAS de 03-01
lineas: pedido.lineas.map(l => ({
nombre: l.nombre,
cantidad: l.cantidad,
precioUnitario: l.precioUnitario,
imagenUrl: porId.get(l.productoId)?.imagenUrl ?? null,
disponibleAhora: porId.get(l.productoId)?.disponible ?? null
}))
});
} catch (err) {
if (err.codigo === 'PEDIDO_NO_EXISTE') return res.status(404).json({ codigo: err.codigo });
next(err);
}
});
const ESTADOS_LEGIBLES = {
PENDIENTE: 'Procesando tu pedido', STOCK_RESERVADO: 'Stock reservado', PAGADO: 'Pago recibido',
CONFIRMADO: 'Confirmado', CANCELADO: 'Cancelado'
};
module.exports = enrutador;Qué hace legítimo este código en un BFF y no en el gateway: sabe qué pantalla lo llama, decide qué degradar si una dependencia falla, traduce estados a textos y campos a nombres de front. Todo eso es conocimiento de presentación, no de negocio (el BFF no cambia el estado del pedido ni recalcula totales), y es del equipo de la app. Cuando la app tenga cinco pantallas así, el esquema GraphQL de 03-03 sustituirá a cinco rutas de composición por un solo endpoint flexible.
- Riesgos del gateway
| Riesgo | Síntoma | Mitigación |
|---|---|---|
| Punto único de fallo | Cae el gateway, cae toda la API aunque los seis servicios estén sanos | Varias réplicas detrás de un balanceador (03-05); gateway sin estado (el rate limit en Redis, no en memoria); /health vigilado |
| Cuello de botella | Toda petición pasa por él: si es lento, todo es lento; si satura CPU, todo espera | Que haga poco (proxy, no parseo de cuerpos); escalado horizontal; medir su latencia añadida (06-04) |
| Gateway "gordo" | Reglas de negocio, transformaciones por cliente, llamadas a BD; todos los equipos tocan su repositorio | La regla del apartado 3; BFFs para lo específico de cada cliente; revisión de cambios por Plataforma |
| Falsa sensación de seguridad | "El gateway ya autentica" y los servicios aceptan cualquier petición interna | Defensa en profundidad (07-01, 07-02): los servicios verifican identidad; el mesh cifra el tráfico interno |
| Acoplamiento al gateway | Los servicios construyen URLs pensando en /api/ o dependen de cabeceras del gateway |
Los servicios exponen sus contratos "limpios"; lo del gateway se queda en el gateway |
| Configuración como código no versionado | Se cambia una regla a mano en producción y nadie sabe qué hay | El YAML de Traefik en el repositorio, desplegado por CI/CD (05-03) |
Errores Comunes y Consejos
express.json()global en el gateway. Consume el stream del cuerpo y el proxy reenvíaPOSTvacíos. Si necesitas parsear en alguna ruta propia del gateway, hazlo solo en esa ruta.- Rutas en orden incorrecto. El comodín
/apiantes que/api/pedidoscaptura todo y el strangler fig deja de funcionar. Específico primero, comodín último (opriorityen Traefik). - Sin timeout en el proxy. Un servicio colgado agota las conexiones del gateway y tumba la API entera.
proxyTimeoutsiempre. - CORS "en todas partes". Con
origin: '*'y credenciales, el navegador lo rechaza y además abres la API a cualquier web. Lista blanca de orígenes. - Rate limit en memoria con varias réplicas. Cada réplica cuenta por su cuenta y el límite real es N veces mayor. Almacén compartido cuando haya más de una instancia.
- Exponer Inventario y Pagos "para depurar" con una ruta temporal. Las rutas temporales se quedan. Para depurar,
kubectl port-forward(05-02) o el panel de administración interno. - Meter la agregación en el gateway "porque solo es una pantalla". La segunda pantalla llega en una semana. BFF desde la primera.
- Un BFF compartido entre web y móvil. Deja de ser "for frontend": vuelve a ser un gateway gordo con otro nombre. Uno por tipo de cliente.
- Olvidar exponer cabeceras en CORS (
exposedHeaders). El navegador ocultaLocation,ETagyX-Request-Ida la web aunque el servidor las envíe, y el polling tras el202no encuentra la URL.
Ejercicios
Ejercicio 1. El equipo de Pedidos ha terminado la extracción y /api/pedidos/* ya apunta al nuevo servicio. Ahora toca /api/clientes/*, pero Marta pide un despliegue prudente: durante una semana, solo las peticiones con la cabecera X-Canary: clientes deben ir al servicio-clientes:3004; el resto, al monolito. Escribe la modificación del gateway Express (una función router de http-proxy-middleware o un middleware previo) y explica cómo lo revertirías en segundos. Menciona qué lección del curso trata este tipo de despliegue en general.
Ejercicio 2. La app móvil necesita una pantalla "mis últimos pedidos" que muestre, para cada uno de los últimos 10 pedidos del cliente, su estado, total y la imagen del primer producto. Diseña la ruta del BFF móvil (GET /movil/mis-pedidos), indica qué llamadas internas hace (con los contratos de 03-01), cuántas son en total con y sin lote, y escribe la respuesta JSON de ejemplo con dos pedidos.
Ejercicio 3. Un desarrollador propone añadir al gateway una comprobación: "si el POST /api/pedidos trae más de 50 líneas, rechazar con 422 antes de llegar a Pedidos, así protegemos al servicio". Argumenta si es responsabilidad del gateway, del servicio de Pedidos, o de ambos, y qué versión de esa idea sí sería aceptable en el gateway.
Soluciones
Solución 1.
http-proxy-middleware acepta en router una función que devuelve el destino por petición:
app.use('/api/clientes', requiereToken, createProxyMiddleware({
target: DESTINOS.monolito, // destino por defecto
changeOrigin: true,
proxyTimeout: 5_000,
router: (req) => req.get('X-Canary') === 'clientes' ? DESTINOS.clientes : DESTINOS.monolito,
pathRewrite: (path, req) => req.get('X-Canary') === 'clientes' ? path.replace(/^\/api/, '') : path,
on: { proxyReq: (proxyReq, req) => proxyReq.setHeader('X-Request-Id', req.requestId) }
}));Hay que declararla antes del comodín /api. Reversión: cambiar router para que devuelva siempre DESTINOS.monolito (o quitar la ruta y dejar que el comodín la absorba) y redesplegar el gateway, o, en Traefik, editar el router con watch: true sin reinicio. Cuando la semana termine bien, se elimina la condición y /api/clientes va siempre al servicio nuevo. Este es un despliegue canary dirigido por cabecera; las estrategias de despliegue (rolling, blue-green, canary por porcentaje de tráfico) se tratan en 05-04.
Solución 2.
Ruta: GET /movil/mis-pedidos (el cliente sale del JWT; en 07-01 el gateway o el BFF lo extraen). Llamadas internas:
GET /pedidos?clienteId=c-1024&orden=-creadoEn&limite=10a Pedidos (paginación por cursor de 03-01) → 1 llamada.- Recoger el
productoIdde la primera línea de cada pedido (hasta 10 ids, deduplicados) y hacerGET /productos?ids=p-501,p-777,...a Catálogo → 1 llamada.
Total: 2 llamadas con lote; 11 sin lote (1 + una por pedido). Respuesta:
{
"pedidos": [
{ "id": "ped-88213", "estado": "CONFIRMADO", "estadoLegible": "Confirmado", "total": 79.70, "creadoEn": "2026-08-15T10:32:07Z", "imagenUrl": "https://cdn.techcorp.example/p-501.webp", "numLineas": 2 },
{ "id": "ped-88102", "estado": "CONFIRMADO", "estadoLegible": "Confirmado", "total": 24.50, "creadoEn": "2026-08-14T18:05:44Z", "imagenUrl": "https://cdn.techcorp.example/p-310.webp", "numLineas": 1 }
],
"siguienteCursor": "eyJjcmVhZG9FbiI6..."
}Si Catálogo falla, imagenUrl va a null y la lista se muestra igual (degradación decidida por el BFF).
Solución 3.
La regla "máximo 50 líneas por pedido" es una regla de negocio del agregado Pedido (02-03): la decide y la aplica el servicio de Pedidos, que responde 422 DATOS_NO_VALIDOS según su contrato (03-01). Si vive en el gateway, el día que el negocio cambie el límite a 100 habrá que redesplegar el gateway, y el equipo de Luis dependerá de Plataforma para una regla suya; además, un POST /reservas interno o el panel de administración, que no pasan por el gateway, no la aplicarían. Es exactamente el gateway "gordo" del apartado 10.
Lo que sí es aceptable en el gateway es una protección transversal sin semántica de negocio: un límite de tamaño de cuerpo (por ejemplo, 256 KB para cualquier POST, con 413 Payload Too Large) que protege a todos los servicios de cuerpos gigantes sin saber qué es una "línea de pedido". El servicio sigue validando las 50 líneas; el gateway solo evita que le lleguen 50 MB.
Conclusión
El API Gateway del puerto 8080 es la única puerta de TechCorp: enruta las rutas públicas /api/productos, /api/pedidos y /api/clientes a sus servicios (y el resto, cada vez menos, al monolito, que es el strangler fig en acción), y concentra lo transversal: correlación con X-Request-Id, CORS, rate limiting, autenticación delegada, timeouts y registro de accesos. Lo hemos construido en Express con http-proxy-middleware para entenderlo y lo hemos declarado en Traefik para operarlo. Y hemos separado con claridad lo que no le corresponde (lógica de negocio, agregaciones a medida) y a quién sí: los backends for frontend, uno por tipo de cliente, mantenidos por el equipo del front, donde encajan tanto la composición REST del bff-movil como el GraphQL de 03-03. Inventario, Pagos y Notificaciones no tienen ruta pública: solo hablan por eventos.
Ahora bien, en todo el código de esta lección hemos escrito destinos como http://servicio-catalogo:3001 como si fueran direcciones fijas. No lo son: en producción habrá ocho réplicas de Catálogo en los picos, cada una con una IP efímera que Kubernetes asigna y destruye. ¿A cuál de ellas llama el gateway? ¿Y Pedidos, cuando consulta el catálogo? ¿Cómo sabe alguien qué instancias están vivas y cuáles no? Eso es el descubrimiento de servicios y el balanceo de carga, la siguiente lección.
Curso de Microservicios
Módulo 1: Introducción a los Microservicios
- Conceptos Básicos de Microservicios
- Ventajas y Desventajas de los Microservicios
- Comparación con la Arquitectura Monolítica
- Cuándo Adoptar Microservicios: Criterios de Decisión
- El Caso Práctico del Curso: la Tienda Online de TechCorp
Módulo 2: Diseño de Microservicios
- Principios de Diseño de Microservicios
- Descomposición de Aplicaciones Monolíticas
- Definición de Bounded Contexts
- Gestión de Datos: una Base de Datos por Servicio
- Consistencia Distribuida: Sagas, CQRS y Event Sourcing
Módulo 3: Comunicación entre Microservicios
- APIs RESTful
- Mensajería Asíncrona
- Protocolos de Comunicación: gRPC, GraphQL
- API Gateway y Backend for Frontend
- Descubrimiento de Servicios y Balanceo de Carga
- Contratos y Versionado de APIs
Módulo 4: Implementación de Microservicios
- Elección de Tecnologías y Herramientas
- Desarrollo de un Microservicio Simple
- Gestión de Configuración
- Integración Práctica: Consumir APIs y Publicar Eventos
- Pruebas en Microservicios: Unitarias, de Integración y de Contrato
Módulo 5: Despliegue y Orquestación
- Contenedores y Docker
- Orquestación con Kubernetes
- CI/CD para Microservicios
- Estrategias de Despliegue: Rolling, Blue-Green y Canary
- Service Mesh: Istio y Linkerd
Módulo 6: Monitoreo y Mantenimiento
- Monitoreo y Logging
- Trazabilidad Distribuida con OpenTelemetry
- Gestión de Errores y Recuperación
- Escalabilidad y Rendimiento
- SLOs, Alertas y Gestión de Incidentes
Módulo 7: Seguridad en Microservicios
- Autenticación y Autorización
- Seguridad en la Comunicación
- Prácticas de Seguridad
- Seguridad en Contenedores y Kubernetes
