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

  1. El problema: muchos servicios, un solo cliente
  2. Responsabilidades transversales del gateway
  3. Lo que un gateway NO debe hacer
  4. Las rutas del gateway de TechCorp y el strangler fig
  5. Opciones de implementación
  6. Un gateway mínimo en Express con http-proxy-middleware
  7. La misma configuración en Traefik (YAML declarativo)
  8. El patrón Backend for Frontend
  9. Un BFF móvil que compone pedido y productos
  10. Riesgos del gateway

  1. 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:

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. El strangler fig es imposible. Si la web habla directamente con el monolito, mover /productos al 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.

  1. 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

  1. 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 precio a price para 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.

  1. 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.

  1. 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.

  1. Un gateway mínimo en Express con http-proxy-middleware

npm install express http-proxy-middleware express-rate-limit cors
// 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 un express.json() global "por costumbre", http-proxy-middleware deja de reenviar el cuerpo y todos los POST llegan vacíos: es el error clásico.
  • pathRewrite es la traducción entre el contrato público (/api/pedidos) y el interno (/pedidos). Los servicios no saben que hay prefijo.
  • proxyTimeout: 5000 es 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.
  • requiereToken es un esqueleto: en 07-01 verificará firma, caducidad y audience del JWT de Keycloak y pasará la identidad al servicio.
  • El error del proxy responde en el mismo formato RFC 7807 de 03-01, con el requestId para 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.

  1. 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).

  1. 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.

  1. 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.

  1. 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ía POST vacíos. Si necesitas parsear en alguna ruta propia del gateway, hazlo solo en esa ruta.
  • Rutas en orden incorrecto. El comodín /api antes que /api/pedidos captura todo y el strangler fig deja de funcionar. Específico primero, comodín último (o priority en Traefik).
  • Sin timeout en el proxy. Un servicio colgado agota las conexiones del gateway y tumba la API entera. proxyTimeout siempre.
  • 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 oculta Location, ETag y X-Request-Id a la web aunque el servidor las envíe, y el polling tras el 202 no 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:

  1. GET /pedidos?clienteId=c-1024&orden=-creadoEn&limite=10 a Pedidos (paginación por cursor de 03-01) → 1 llamada.
  2. Recoger el productoId de la primera línea de cada pedido (hasta 10 ids, deduplicados) y hacer GET /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

Módulo 2: Diseño de Microservicios

Módulo 3: Comunicación entre Microservicios

Módulo 4: Implementación de Microservicios

Módulo 5: Despliegue y Orquestación

Módulo 6: Monitoreo y Mantenimiento

Módulo 7: Seguridad en Microservicios

Módulo 8: Casos de Estudio y Ejemplos Prácticos

© Copyright 2026. Todos los derechos reservados