Repasa lo que escribimos en el módulo 4: un limitador de peticiones con Redis, la validación de tokens JWT y de tokens OAuth por JWKS, una lista blanca de orígenes para CORS, cabeceras de caché con ETag y 304, compresión, métricas en /metricas. Seis piezas de infraestructura, con su código, sus pruebas y su mantenimiento.

Ahora la incomodidad: casi todas existen resueltas una capa por encima. Un API gateway hace las seis, con configuración en lugar de código, y las hace para todas tus APIs a la vez. La pregunta obvia es si perdimos el tiempo. La respuesta es que no —entender un mecanismo es lo que permite decidir si delegarlo y depurarlo cuando falle—, pero la pregunta siguiente sí es difícil y es la que ocupa media lección: qué se delega en el gateway y qué debe quedarse en la aplicación.

Y hay una segunda mitad. Tenemos una API excelente, documentada y desplegada. Pero CataBox, la aplicación de terceros que se integra por OAuth, no tiene ni idea de cómo empezar: dónde registrarse, cómo conseguir credenciales, dónde probar sin romper nada, qué límites tiene, cuándo cambiará algo. Eso es un portal de desarrollador, y determina si tu API se usa o se abandona en la primera hora.

Esta lección cierra el módulo 5 con las dos capas que rodean a la API: la que la protege y la que la explica.

Contenido

  1. Qué es un API gateway y qué problema resuelve
  2. Las funciones que asume, comparadas con el módulo 4
  3. Qué delegar y qué no delegar nunca
  4. La arquitectura completa: CDN, gateway, servicios
  5. El patrón backend for frontend
  6. El service mesh es otra cosa
  7. Los productos y cómo se comparan
  8. Configuración de Kong para Tienda Aroma
  9. La alternativa con NGINX
  10. Qué middlewares podríamos retirar y cuáles no
  11. Los riesgos del gateway
  12. Qué es un portal de desarrollador
  13. Qué contiene un buen portal
  14. Registro de aplicaciones y credenciales OAuth
  15. El tiempo hasta la primera llamada con éxito
  16. Ciclo de vida e inventario de APIs
  17. Monetización, como nota
  18. Balance del módulo 5

  1. Qué es un API gateway y qué problema resuelve

Un API gateway es un servidor que se sitúa delante de una o varias APIs y actúa como puerta única: recibe todo el tráfico externo, aplica políticas transversales y reenvía las peticiones al servicio que corresponda.

El problema que resuelve se ve mejor con el escenario que le da sentido. Imagina que Tienda Aroma crece:

Sin gateway                          Con gateway
─────────────────────────────        ─────────────────────────────
api.tiendaaroma.example              api.tiendaaroma.example
  → API de tienda                      → GATEWAY
     · rate limiting propio                 · rate limiting (una vez)
     · CORS propio                          · CORS (una vez)
     · JWT propio                           · JWT (una vez)
                                            · métricas (una vez)
inventario.internal                         ↓
  → API de inventario                  /v1/cafes    → API de tienda
     · rate limiting propio            /v1/pedidos  → API de tienda
     · CORS propio                     /v1/stock    → API de inventario
     · JWT propio (¿igual?)            /v1/sugerencias → recomendaciones
recomendaciones.internal
  → API de recomendaciones
     · rate limiting propio (¿o no?)

Con una API, el gateway aporta poco: estás moviendo de sitio código que ya funciona. Con cinco, la diferencia es enorme: sin él, cada equipo reimplementa el rate limiting a su manera, tres de los cinco lo hacen mal, la política de CORS diverge, y no hay un sitio donde responder «¿cuántas peticiones recibimos en total?».

La formulación precisa: un gateway centraliza las preocupaciones transversales del tráfico. Y como todo lo centralizado, aporta consistencia y crea un punto único de fallo. Ambas cosas a la vez.

  1. Las funciones que asume, comparadas con el módulo 4

Función Cómo lo hicimos nosotros Qué hace el gateway ¿Delegable?
Rate limiting express-rate-limit + Redis (04-04) Plugin con límites por consumidor, ruta y nivel Sí, totalmente
Terminación TLS Delegada al proxy Certificados, renovación, versiones de TLS
Validación de JWT middleware/autenticacion.js (03-06) Verifica firma, exp, iss, aud y rechaza antes de llegar a ti Sí, la validación
OAuth y JWKS autenticacion-oauth.js + jose (04-03) Descarga y cachea el JWKS, valida ámbitos
CORS cors(opcionesCors) (04-05) Lista blanca declarativa, preflight
Caché de respuestas ETag + Redis cache-aside (04-06) Caché de respuestas por URI y Vary Parcialmente
Compresión compression gzip y brotli, normalmente mejor implementado
Enrutado y versionado app.use('/v1', rutasV1) Ruta a servicio; /v1 y /v2 a destinos distintos
Transformación Mapeadores (03-03) Añadir, quitar o renombrar cabeceras y campos Sí, con cuidado
Agregación No la hacemos Combinar varias llamadas en una respuesta Con mucho cuidado
Cuotas por cliente No las hacemos 10.000 llamadas al mes por plan
Métricas y logs prom-client + pino (04-07) Métricas por consumidor y ruta, sin tocar código Sí, como complemento
mTLS No lo hacemos Certificados de cliente para socios
Listas negras No las hacemos Bloqueo por IP, país, patrón o reputación
Reintentos y circuit breaker Parcial (04-04) Reintentos, timeouts y cortocircuito por destino
Validación de esquema Zod (03-04) Validación contra el JSON Schema del contrato Parcialmente
Autorización por recurso exigirRol, comprobación de propiedad No puede saberlo NUNCA
Lógica de negocio Servicios No puede NUNCA
Validación semántica Servicios No puede NUNCA

Las tres últimas filas son el asunto del apartado siguiente.

  1. Qué delegar y qué no delegar nunca

El criterio, en una frase: el gateway sabe quién llama y a dónde; solo la aplicación sabe qué hay dentro y qué significa.

De ahí se deduce todo lo demás:

Se delega bien lo que depende únicamente de la petición y de la identidad: rate limiting, terminación TLS, verificación criptográfica del token, CORS, compresión, enrutado. Son decisiones que no requieren consultar tu base de datos.

No se delega nunca, y conviene ser tajante:

1. La autorización a nivel de recurso. El gateway puede comprobar que el token es válido y que tiene el ámbito pedidos.leer. No puede comprobar que ped_5001 pertenece a cli_842, porque eso exige consultar la base de datos. Y esa comprobación es precisamente la que evita el fallo número uno del OWASP API Top 10 (04-02), el BOLA. Si dejas de hacerla en la aplicación porque "ya lo mira el gateway", tienes una vulnerabilidad crítica.

// src/servicios/pedidos.js — esto se queda en la aplicación, SIEMPRE.
export async function obtenerPedido(pedidoId, usuario) {
  const pedido = await repositorioPedidos.buscarPorId(pedidoId);
  if (!pedido) throw errores.pedidoNoEncontrado(pedidoId);

  // El gateway ya validó el token y el ámbito. Lo que NO puede saber
  // es de quién es este pedido: solo la base de datos lo sabe.
  const esSuyo = pedido.clienteId === usuario.id;
  const esPersonal = ['empleado', 'administrador'].includes(usuario.rol);
  if (!esSuyo && !esPersonal) throw errores.permisosInsuficientes();

  return pedido;
}

2. La lógica de negocio. Que un pedido pagado no pueda volver a pendiente_pago, que no se venda más stock del disponible, que el plazo de devolución sean 14 días. Meter reglas de negocio en la configuración del gateway crea un segundo lugar donde vive el dominio, sin pruebas, sin tipos y sin revisión de código. Es el error más caro que se comete con estas herramientas.

3. La validación semántica. El gateway puede rechazar un cuerpo que no cumpla el JSON Schema —tipos, rangos, campos obligatorios— y es una defensa útil. No puede validar que cafeId exista, que precioMin sea menor que precioMax, o que el cliente pueda comprar ese café.

4. La defensa en profundidad. Aunque el gateway valide el token, la aplicación debe seguir validándolo. Motivo: si alguien alcanza tu servicio esquivando el gateway —una regla de red mal puesta, un despliegue nuevo, un atacante dentro de la red— tu API quedaría totalmente abierta. La regla es zero trust: el servicio no confía en que el tráfico venga de donde parece.

Una tabla que resume la asignación:

Pregunta Quién responde
¿Este token está firmado por quien dice y no ha caducado? Gateway (y también la app)
¿Este consumidor ha superado su cuota? Gateway
¿Este origen puede hacer peticiones desde el navegador? Gateway
¿Este token tiene el ámbito pedidos.escribir? Gateway (y también la app)
¿Este usuario puede ver este pedido? Solo la aplicación
¿Hay stock suficiente para esta línea? Solo la aplicación
¿El pedido está en un estado que admite pago? Solo la aplicación

  1. La arquitectura completa: CDN, gateway, servicios

graph LR
    SPA[SPA<br/>tiendaaroma.example] --> CDN
    MOV[Aroma Móvil] --> CDN
    PAN[Panel interno] --> CDN
    CAT[CataBox<br/>OAuth] --> CDN
    RE[RápidoEnvíos<br/>mTLS] --> CDN
    CDN[CDN y WAF<br/>TLS, DDoS, estáticos, caché de borde] --> GW
    GW[API Gateway<br/>rate limiting, JWT, CORS,<br/>enrutado, cuotas, métricas]
    GW --> API[API Tienda Aroma<br/>rutas /v1/cafes y /v1/pedidos]
    GW --> INV[Servicio de inventario]
    GW --> REC[Servicio de recomendaciones]
    API -.gRPC.-> INV
    API -.gRPC.-> REC
    API --> BD[(SQLite o PostgreSQL)]
    API --> R[(Redis)]

Qué hace cada capa y por qué está donde está:

Capa Responsabilidad Por qué ahí
CDN / WAF TLS, mitigación de DDoS, estáticos, caché de borde, bloqueo geográfico Lo más cerca posible del usuario y lo más lejos posible de tu infraestructura. El tráfico malicioso se filtra antes de consumir recursos tuyos.
Gateway Autenticación, rate limiting, CORS, enrutado, cuotas, métricas por consumidor Un solo punto que conoce a todos los consumidores y todos los servicios
Aplicación Lógica de negocio, autorización por recurso, validación semántica, persistencia Es lo único que conoce el dominio
Este-oeste (gRPC) Comunicación entre servicios internos No pasa por el gateway: sería un rodeo innecesario y un cuello de botella

Las flechas punteadas del diagrama son importantes: el tráfico norte-sur (de fuera hacia dentro) pasa por el gateway; el tráfico este-oeste (entre servicios internos) no. Hacer que las llamadas internas salgan y vuelvan a entrar por el gateway multiplica la latencia y convierte el gateway en el cuello de botella de todo el sistema.

  1. El patrón backend for frontend

Aroma Móvil tiene un problema que la SPA no tiene: para pintar la pantalla de inicio necesita el catálogo destacado, los pedidos recientes del cliente y sus preferencias. Con la API tal cual, son tres peticiones, sobre una red móvil con 150 ms de latencia y una batería que se gasta.

Un BFF es una API intermedia dedicada a un consumidor concreto, que agrega y adapta:

// bff-movil/src/rutas/inicio.js
// El BFF de Aroma Móvil: UNA llamada del móvil, tres en la red interna (rápidas).
router.get('/inicio', autenticar, asincrono(async (req, res) => {
  const [destacados, pedidos, preferencias] = await Promise.all([
    apiTienda.obtenerCafes({ destacado: true, limite: 6 }),
    apiTienda.obtenerPedidosDeCliente(req.usuario.id, { limite: 3 }),
    apiTienda.obtenerPreferencias(req.usuario.id),
  ]);

  // Además de agregar, ADELGAZA: el móvil no necesita notasCata ni _links
  // en la pantalla de inicio, y cada byte cuenta en una red móvil.
  res.json({
    destacados: destacados.datos.map((c) => ({
      id: c.id, nombre: c.nombre, precioEuros: c.precioEuros, imagen: c._links.imagen.href,
    })),
    pedidosRecientes: pedidos.datos.map((p) => ({
      id: p.id, estado: p.estado, totalEuros: p.totalEuros, fecha: p.fechaCreacion,
    })),
    tuestePreferido: preferencias.tueste,
  });
}));
API general BFF
Consumidores Todos Uno
Quién lo mantiene Equipo de plataforma El equipo del cliente que sirve
Puede cambiar Con ciclo de deprecación (02-07) Cuando quiera, junto con su cliente
Riesgo Duplicar lógica de negocio en cada BFF

La ventaja política del BFF es tan importante como la técnica: el equipo móvil puede cambiar su BFF sin negociar con nadie ni esperar un ciclo de versionado, porque es el único consumidor. La trampa es que un BFF acabe con reglas de negocio propias que divergen de la API. Regla: el BFF agrega, filtra y adapta formatos; no decide nada del dominio.

Algunos gateways ofrecen agregación por configuración, sin escribir un BFF. Funciona para casos triviales y se vuelve inmanejable en cuanto hay lógica condicional. Si necesitas un if, escribe un BFF.

  1. El service mesh es otra cosa

Se confunden constantemente, así que conviene separarlos:

API gateway Service mesh
Tráfico Norte-sur: de fuera hacia dentro Este-oeste: entre servicios internos
Dónde vive Un punto de entrada centralizado Un sidecar junto a cada servicio
Qué resuelve Autenticación externa, cuotas, exposición pública mTLS interno, reintentos, reparto de tráfico, observabilidad
Ejemplos Kong, Apigee, AWS API Gateway Istio, Linkerd, Consul
Cuándo hace falta En cuanto expones una API a terceros Con muchos servicios internos y equipos que los operan

No son alternativas: son complementarios, y muchas arquitecturas maduras tienen ambos. Para Tienda Aroma hoy, con una API y dos servicios internos, un service mesh es claramente prematuro: su coste operativo solo se amortiza con decenas de servicios.

  1. Los productos y cómo se comparan

Producto Modelo Extensibilidad Coste Portal incluido Encaje con Tienda Aroma
Kong Autogestionado (OSS) o gestionado Alta: plugins en Lua, JS, Python, Go Gratis (OSS) / de pago Sí, en la edición de pago Buena opción: potente y con salida hacia lo gestionado
NGINX Autogestionado Media: módulos, Lua con OpenResty Gratis / NGINX Plus No Si ya lo tienes de proxy y necesitas poco más
Traefik Autogestionado Media Gratis / de pago No Excelente en Docker y Kubernetes: descubre servicios solo
AWS API Gateway Gestionado Media: Lambda como autorizador Por petición Sí, básico Natural si ya estás en AWS; el coste escala con el tráfico
Apigee (Google) Gestionado Muy alta Alto Sí, muy completo Empresa grande con monetización y muchos socios
Azure API Management Gestionado Alta: políticas XML Medio-alto Sí, muy completo Ecosistema Microsoft
Tyk Ambos Alta Gratis (OSS) / de pago Alternativa sólida a Kong, portal incluido en OSS
Cloudflare / Fastly Gestionado, en el borde Media: Workers Bajo-medio Parcial Ya lo tienes como CDN; muchas funciones de gateway al borde

Criterios de elección, que se parecen mucho a los de 05-03:

  • ¿Quién lo va a operar? Un Kong autogestionado en alta disponibilidad es un sistema distribuido más que mantener, parchear y monitorizar. Si el equipo es pequeño, la opción gestionada casi siempre gana.
  • ¿Dónde está el resto? Si ya estás en AWS con ECS, AWS API Gateway ahorra integración. Si ya usas Cloudflare, buena parte del trabajo puede hacerse ahí.
  • ¿Necesitas un portal? Si vas a tener terceros como CataBox, el portal es la mitad del producto. Kong OSS no lo trae; Tyk sí; Apigee y Azure APIM traen los más completos.
  • ¿Cuánto cuesta por petición? Los gestionados cobran por millón de peticiones. Con volúmenes altos, la cuenta puede superar al coste de operarlo tú.
  • ¿Qué modelo de configuración? La configuración declarativa en YAML versionada en Git (Kong con decK, Traefik, Gateway API de Kubernetes) es muy superior a configurar por interfaz gráfica: se revisa en un pull request y se despliega con la tubería de 05-05.

  1. Configuración de Kong para Tienda Aroma

Configuración declarativa completa, versionable en el repositorio como gateway/kong.yaml:

# gateway/kong.yaml — configuración declarativa del gateway de Tienda Aroma.
# Se aplica con: deck gateway sync gateway/kong.yaml
# Vive en el repositorio y se despliega desde la tubería de CI (05-05).
_format_version: "3.0"

# ---------------------------------------------------------------------------
# SERVICIOS: los destinos internos. El gateway no los expone directamente.
# ---------------------------------------------------------------------------
services:
  - name: api-tienda-aroma
    url: http://api-tienda-aroma.interno:3000
    retries: 2                    # reintentos ante fallo de conexión
    connect_timeout: 2000
    write_timeout: 10000
    read_timeout: 10000

    routes:
      # Ruta principal: todo /v1 va a la API. El versionado en la ruta (02-07)
      # permite que mañana /v2 apunte a otro servicio sin tocar nada más.
      - name: v1
        paths: ["/v1"]
        strip_path: false         # la API espera recibir /v1: NO lo quitamos
        protocols: ["https"]      # solo HTTPS; el HTTP se redirige antes
        methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]

      # El login tiene su propia ruta para poder aplicarle un límite distinto.
      - name: v1-sesiones
        paths: ["/v1/sesiones"]
        strip_path: false
        protocols: ["https"]
        methods: ["POST"]

      # La documentación (05-02) es pública y no lleva autenticación.
      - name: documentacion
        paths: ["/docs"]
        strip_path: false
        protocols: ["https"]
        methods: ["GET", "HEAD"]

# ---------------------------------------------------------------------------
# CONSUMIDORES: quién llama. Permite límites y cuotas por cliente.
# ---------------------------------------------------------------------------
consumers:
  - username: spa-tiendaaroma
    tags: ["interno", "primera-parte"]
  - username: aroma-movil
    tags: ["interno", "primera-parte"]
  - username: panel-interno
    tags: ["interno"]
  - username: rapidoenvios
    tags: ["socio"]
  - username: catabox
    tags: ["tercero", "plan-gratuito"]

# ---------------------------------------------------------------------------
# PLUGINS GLOBALES: se aplican a todo el tráfico.
# ---------------------------------------------------------------------------
plugins:
  # --- Rate limiting: los mismos límites que 04-04, ahora declarativos ------
  - name: rate-limiting
    config:
      minute: 600                 # el límite global de 04-04
      hour: 20000
      policy: redis               # estado compartido entre instancias del gateway
      redis:
        host: redis.interno
        port: 6379
        database: 1               # base distinta a la de la aplicación
      fault_tolerant: true        # si Redis cae, DEJA PASAR en lugar de bloquear
      hide_client_headers: false
      limit_by: consumer          # por consumidor autenticado, no por IP
      error_message: '{"error":{"codigo":"limite_peticiones","mensaje":"Has superado el límite de peticiones.","detalles":[]}}'

  # --- Correlación de trazas: la cabecera de 04-07 --------------------------
  - name: correlation-id
    config:
      header_name: Aroma-Traza-Id
      generator: uuid
      echo_downstream: true       # también se devuelve al cliente

  # --- Observabilidad: métricas por servicio, ruta y consumidor -------------
  - name: prometheus
    config:
      per_consumer: true          # métricas segmentadas por consumidor
      status_code_metrics: true
      latency_metrics: true
      # Cuidado con la cardinalidad (04-07): per_consumer está bien porque
      # los consumidores son decenas; NUNCA etiquetar por usuario final.

  - name: http-log
    config:
      http_endpoint: http://coleccion-logs.interno:9880/kong
      custom_fields_by_lua:
        traza_id: "return kong.request.get_header('Aroma-Traza-Id')"

  # --- Compresión y cabeceras de respuesta ---------------------------------
  - name: response-transformer
    config:
      remove:
        headers: ["Server", "X-Powered-By"]   # no revelar la pila (04-02)
      add:
        headers:
          - "Strict-Transport-Security: max-age=31536000; includeSubDomains"
          - "X-Content-Type-Options: nosniff"

# ---------------------------------------------------------------------------
# PLUGINS POR RUTA
# ---------------------------------------------------------------------------
  # --- CORS: la lista blanca de 04-05, ahora en el gateway -----------------
  - name: cors
    route: v1
    config:
      origins:
        - https://tiendaaroma.example
        - https://panel.tiendaaroma.example
        # NUNCA "*" junto con credentials: true. Es la regla de oro de 04-05.
      methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
      headers:
        - Authorization
        - Content-Type
        - Idempotency-Key
        - If-Match
        - If-None-Match
      exposed_headers:            # sin esto, el navegador no las ve
        - ETag
        - Link
        - Location
        - Retry-After
        - Aroma-Traza-Id
        - Aroma-RateLimit-Limite
        - Aroma-RateLimit-Restantes
        - Aroma-RateLimit-Reinicio
      credentials: true
      max_age: 3600               # cachea el preflight una hora
      preflight_continue: false   # el gateway responde el OPTIONS: la app ni se entera

  # --- Validación de JWT y OAuth (03-06 y 04-03) ---------------------------
  - name: jwt-signer               # valida contra el JWKS del proveedor OIDC
    route: v1
    config:
      access_token_issuer: https://auth.tiendaaroma.example
      access_token_jwks_uri: https://auth.tiendaaroma.example/.well-known/jwks.json
      access_token_leeway: 5      # tolerancia de reloj, en segundos
      verify_access_token_signature: true
      verify_access_token_expiry: true
      verify_access_token_issuer: true
      # Propaga hacia la API la identidad ya verificada, en una cabecera propia.
      # La aplicación SIGUE validando el token: defensa en profundidad.
      upstream_access_token_header: Authorization

  # --- Límite específico del login: mucho más estricto (04-04) -------------
  - name: rate-limiting
    route: v1-sesiones
    config:
      minute: 5                   # cinco intentos por minuto contra fuerza bruta
      policy: redis
      redis: { host: redis.interno, port: 6379, database: 1 }
      limit_by: ip                # aquí sí por IP: aún no hay consumidor autenticado
      fault_tolerant: false       # si Redis cae, MEJOR BLOQUEAR que dejar pasar
      error_message: '{"error":{"codigo":"limite_peticiones","mensaje":"Demasiados intentos de inicio de sesión.","detalles":[]}}'

  # --- La documentación no lleva autenticación -----------------------------
  - name: request-termination
    route: documentacion
    enabled: false                # marcador: aquí NO se aplica jwt-signer

  # --- Cuota mensual para terceros (CataBox) -------------------------------
  - name: rate-limiting-advanced
    consumer: catabox
    config:
      limit: [100, 10000]
      window_size: [60, 2592000]  # 100/minuto y 10.000/mes: el plan gratuito
      identifier: consumer
      strategy: redis
      sync_rate: 1

  # --- Socio logístico: límites amplios y mTLS -----------------------------
  - name: mtls-auth
    consumer: rapidoenvios
    config:
      ca_certificates: ["<id-del-ca-de-socios>"]
      skip_consumer_lookup: false
      revocation_check_mode: STRICT

Cuatro puntos de esa configuración que merecen comentario:

  • fault_tolerant distinto según la ruta. En el límite global es true: si Redis cae, preferimos dejar pasar tráfico a tirar la API entera. En el login es false: si Redis cae, preferimos bloquear a quedarnos sin protección contra fuerza bruta. Es una decisión de disponibilidad frente a seguridad que debe tomarse conscientemente para cada caso, y el gateway obliga a explicitarla.
  • limit_by: consumer frente a limit_by: ip. Limitar por IP castiga a todos los usuarios detrás de un NAT corporativo y no protege de un atacante con muchas IP. Por consumidor autenticado es lo correcto… salvo en el login, donde todavía no hay consumidor.
  • exposed_headers. El error de CORS más frustrante de 04-05: el navegador recibe ETag pero JavaScript no puede leerlo si no está en Access-Control-Expose-Headers. Aquí queda declarado de una vez para toda la API.
  • preflight_continue: false. El gateway responde los OPTIONS y la aplicación no los ve. Ahorra latencia y una parte del tráfico, pero significa que tu middleware de CORS deja de ejecutarse para el preflight: si algún día quitas el gateway, recuerda que esa pieza estaba ahí.

La configuración se aplica desde la tubería de 05-05:

# Validar antes de aplicar (en el pull request)
deck gateway validate gateway/kong.yaml

# Ver qué cambiaría (en el pull request: el diff se comenta en el PR)
deck gateway diff gateway/kong.yaml

# Aplicar (tras aprobación, en el despliegue)
deck gateway sync gateway/kong.yaml

Esto es infraestructura como código aplicada al gateway, y es lo que evita el problema clásico: alguien toca la configuración en la interfaz gráfica un martes, nadie lo recuerda, y en la siguiente reconstrucción del entorno la política desaparece.

  1. La alternativa con NGINX

Si el equipo ya opera NGINX y no necesita gestión de consumidores ni portal, buena parte de lo anterior se consigue con configuración directa:

# gateway/nginx.conf — versión mínima con NGINX
# Zonas de memoria compartida para los limitadores. 10 MB ≈ 160.000 IP.
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

upstream api_tienda_aroma {
    server api-1.interno:3000 max_fails=3 fail_timeout=10s;
    server api-2.interno:3000 max_fails=3 fail_timeout=10s;
    keepalive 32;                     # conexiones persistentes: menos latencia
}

server {
    listen 443 ssl http2;
    server_name api.tiendaaroma.example;

    ssl_certificate     /etc/ssl/certs/tiendaaroma.crt;
    ssl_certificate_key /etc/ssl/private/tiendaaroma.key;
    ssl_protocols       TLSv1.2 TLSv1.3;   # nada por debajo de 1.2 (04-02)

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    server_tokens off;                # no revelar la versión de NGINX

    client_max_body_size 100k;        # el mismo límite que express.json (04-02)

    # Traza de correlación: se genera si no viene, y se propaga (04-07)
    set $traza_id $http_aroma_traza_id;
    if ($traza_id = "") { set $traza_id $request_id; }

    location /v1/sesiones {
        limit_req zone=login burst=3 nodelay;
        limit_req_status 429;
        proxy_pass http://api_tienda_aroma;
        include /etc/nginx/proxy_comun.conf;
    }

    location /v1/ {
        limit_req zone=general burst=20 nodelay;
        limit_req_status 429;

        gzip on;
        gzip_types application/json;
        gzip_min_length 1024;

        proxy_pass http://api_tienda_aroma;
        include /etc/nginx/proxy_comun.conf;
    }

    location /docs {
        proxy_pass http://api_tienda_aroma;
        include /etc/nginx/proxy_comun.conf;
    }
}
# /etc/nginx/proxy_comun.conf — cabeceras que SIEMPRE hay que propagar
proxy_http_version 1.1;
proxy_set_header Connection "";                       # habilita keepalive
proxy_set_header Host              $host;
proxy_set_header X-Real-IP         $remote_addr;
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host  $host;
proxy_set_header Aroma-Traza-Id    $traza_id;
proxy_read_timeout 10s;
proxy_connect_timeout 2s;

La cabecera X-Forwarded-For conecta directamente con el trust proxy 1 de la posición 1 de src/app.js. Sin X-Forwarded-For, Express ve la IP del gateway en todas las peticiones y el rate limiting por IP limita al gateway entero. Y con trust proxy mal configurado —confiando en más saltos de los que hay— un atacante puede falsificar su IP añadiendo la cabecera él mismo. El número debe coincidir exactamente con la cantidad de proxies de confianza que hay delante.

Lo que NGINX no da y Kong sí: gestión de consumidores con credenciales, cuotas mensuales, validación de JWT sin recurrir a Lua, portal de desarrollador y configuración por API en lugar de por fichero. Para una API con socios y terceros, esas ausencias pesan.

  1. Qué middlewares podríamos retirar y cuáles no

La pregunta práctica: con el gateway de arriba delante, ¿qué queda en src/app.js?

Posición en src/app.js Middleware ¿Retirar? Razón
1 disable('x-powered-by') + trust proxy No, y trust proxy es más necesario Sin él, todas las IP son la del gateway
2 asignarTrazaId No, adaptar Debe respetar el Aroma-Traza-Id que llega y generarlo solo si falta
3 cabecerasSeguridad (helmet) No Defensa en profundidad; el coste es nulo
4 cors(opcionesCors) Sí, con condiciones El gateway lo hace y responde el preflight. Mantenerlo si la app se expone también sin gateway (desarrollo local)
5 registrarPeticiones (pino) No Los logs del gateway no tienen contexto de negocio: usuario, pedido, consulta
6 metricasMiddleware No Las del gateway son de tráfico; las tuyas son de negocio (aroma_pedidos_creados_total)
7 limiteGlobal Se puede simplificar Delegar el global; mantener un límite de seguridad más laxo por si alguien esquiva el gateway
8 compression El gateway comprime mejor y libera CPU de la aplicación
9 express.json({limit}) No El límite de cuerpo se aplica en ambos: defensa en profundidad
10-11 /salud y /salud/preparado No Los usa el orquestador (05-05), no el gateway
12 /metricas protegido No Prometheus raspa la instancia, no el gateway
13 etagCondicional No El ETag depende del contenido: solo la app sabe generarlo
14 autenticar en las rutas No, jamás Zero trust: si alguien esquiva el gateway, esto es lo único que queda
14 exigirRol / exigirAmbito No, jamás Autorización: nunca se delega
14 exigirClaveIdempotencia No Requiere estado de negocio (03-05)
14 validar(esquema) No La validación semántica es de la aplicación
15-16 manejadorNoEncontrado, manejadorErrores No El formato del catálogo (02-04) es tuyo

Balance honesto: se retiran uno o dos middlewares de dieciséis. Ese es el resultado real y conviene decirlo sin adornos, porque contradice la promesa comercial de estas herramientas.

El valor del gateway no está en adelgazar tu aplicación. Está en tres cosas distintas:

  1. Consistencia entre varios servicios. Con cinco APIs, las políticas se escriben una vez en lugar de cinco.
  2. Gestión de consumidores. Cuotas por plan, credenciales, mTLS con socios, listas negras. Eso no existía en nuestro proyecto y construirlo sería costoso.
  3. Cambiar políticas sin desplegar. Ajustar un límite o bloquear a un cliente abusivo es una línea de YAML y treinta segundos, en lugar de un despliegue completo.

Y una última observación importante: la configuración del gateway hay que probarla igual que el código. El recorrido de Newman de 05-01 debe ejecutarse contra el gateway, no contra la API directamente. Si no, un CORS mal configurado o un strip_path equivocado se descubre en producción. Con deck gateway diff en el pull request, el cambio de política se revisa como cualquier otro.

  1. Los riesgos del gateway

1. Punto único de fallo. Si el gateway cae, todas tus APIs caen a la vez, aunque estén perfectamente sanas. Mitigación: varias instancias, comprobaciones de salud, y —lo que casi nadie hace— un plan documentado para exponer temporalmente los servicios sin él.

2. Cuello de botella y latencia añadida. Cada petición atraviesa un salto de red y un procesamiento adicional: entre 1 y 10 ms típicamente. Con un presupuesto de latencia de 200 ms (04-06) es asumible; si tu p99 objetivo son 20 ms, es un 25 % del presupuesto y hay que medirlo, no suponerlo.

3. Configuración duplicada y divergente. El caso más pernicioso: CORS configurado en el gateway y en la aplicación con listas distintas. Un origen funciona en desarrollo y falla en producción, o al revés, y depurarlo es un infierno porque cada capa dice que está bien. Regla: una política, un dueño, documentado en un ADR (04-01).

4. Lógica de negocio en el gateway. El riesgo más caro. Empieza con una transformación inocente y acaba con reglas de precios en un script Lua sin pruebas, sin control de versiones efectivo y sin nadie que sepa que están ahí. La regla del apartado 3 no admite excepciones.

5. Dependencia del proveedor. Las políticas de Apigee o de Azure APIM no se migran a Kong. Cuanto más lógica metas en el gateway, más caro es cambiarlo. Otro argumento para mantenerlo fino.

6. Falsa sensación de seguridad. «El gateway valida los tokens, así que la app puede confiar.» No. Zero trust: la aplicación valida siempre.

7. Depuración más difícil. Un 403 puede venir del gateway o de la aplicación, y a veces no es evidente cuál. Mitigación: que el gateway marque sus propios errores —una cabecera propia, o un campo en detalles— y que el Aroma-Traza-Id atraviese ambas capas, que es exactamente para lo que sirve el plugin correlation-id.

  1. Qué es un portal de desarrollador

Cambiamos de tema y de audiencia. Un portal de desarrollador es el sitio web donde quien quiere integrarse con tu API aprende a hacerlo, se registra, obtiene credenciales, prueba y se entera de los cambios.

La pregunta que responde: CataBox quiere integrarse con Tienda Aroma un martes por la mañana. ¿Qué pasa?

Sin portal Con portal
Busca en Google y encuentra un PDF de 2024 Encuentra developers.tiendaaroma.example
Escribe a soporte pidiendo la documentación Lee la referencia generada de openapi.yaml
Espera dos días a que alguien responda Se registra y crea una aplicación en cinco minutos
Pide credenciales por correo Obtiene client_id y un entorno de pruebas al instante
Alguien crea el cliente OAuth a mano Prueba en la consola interactiva sin escribir código
Descubre los límites cuando le devuelven 429 Los lee en la página de planes
Se entera de una deprecación cuando algo se rompe Está suscrito al changelog

Quién lo necesita: cualquier API con consumidores que no estén en tu equipo. Con una API estrictamente interna y dos equipos, /docs con Swagger UI y un canal de chat pueden bastar. En cuanto hay terceros, socios o más de cuatro o cinco equipos internos, el portal deja de ser un lujo.

  1. Qué contiene un buen portal

Sección Contenido De dónde sale
Inicio Qué hace la API, para quién, un ejemplo en 30 segundos Escrito a mano
Guía de inicio De cero a la primera llamada con éxito Escrito a mano — la sección más importante
Referencia Todos los endpoints, parámetros, esquemas y errores Generada de openapi.yaml (05-02)
Autenticación JWT frente a OAuth, flujos, ámbitos, renovación Escrito, con los securitySchemes como base
Guías temáticas Paginación, idempotencia, webhooks, errores, reintentos Escrito a mano
Consola interactiva "Probar ahora" contra el entorno de pruebas Swagger UI, Scalar o Stoplight Elements
Mis aplicaciones Registro, credenciales, ámbitos, URL de retorno Gateway o portal
Entorno de pruebas Datos ficticios, credenciales de prueba, tarjetas de prueba Infraestructura
Límites y planes Cuotas, precios, cómo pedir más Escrito
Changelog Qué cambió y cuándo; avisos de deprecación Del ciclo de 02-07
Estado del servicio Incidencias y ventanas de mantenimiento Monitorización (04-07)
Soporte Cómo pedir ayuda y qué información aportar Escrito
Términos de uso Qué se puede hacer con los datos; RGPD Legal

Tres apartados suelen faltar y son los que más se agradecen:

  • Guías temáticas, no solo referencia. La referencia dice que POST /v1/pedidos acepta Idempotency-Key. Una guía explica por qué, qué pasa al reintentar, cuánto tiempo se conserva la clave y cómo generarla. La referencia responde "qué"; la guía responde "cómo y por qué", y es lo que evita las integraciones mal hechas.
  • La página de errores. El catálogo completo de 02-04 con qué significa cada código, si conviene reintentar y qué hacer. Con 429 y Retry-After, con 409 de idempotencia, con 412 de If-Match. Es la página que más visitas recibe de un portal maduro, y su ausencia se traduce directamente en tickets de soporte.
  • Ejemplos ejecutables. curl copiable, y la colección de Postman de 05-01 con un botón de importar. Que alguien pueda pegar un comando y ver una respuesta real en treinta segundos vale más que diez páginas de prosa.

Y una guía de inicio que funciona tiene esta forma, sin adornos:

# Primeros pasos con la API de Tienda Aroma

## 1. Crea tu cuenta y tu aplicación (2 minutos)
Regístrate y crea una aplicación en "Mis aplicaciones".
Obtendrás un `client_id` y, si es una aplicación confidencial, un `client_secret`.

## 2. Obtén un token de prueba (1 minuto)
    curl -X POST https://auth.pruebas.tiendaaroma.example/oauth/token \
      -d "grant_type=client_credentials" \
      -d "client_id=TU_CLIENT_ID" \
      -d "client_secret=TU_CLIENT_SECRET" \
      -d "scope=cafes.leer"

## 3. Tu primera llamada (30 segundos)
    curl https://api.pruebas.tiendaaroma.example/v1/cafes?limite=3 \
      -H "Authorization: Bearer TU_TOKEN"

Deberías ver tres cafés del catálogo de pruebas. **Ya está.**

## 4. Siguientes pasos
- [Filtrar, ordenar y paginar](/guias/consultas)
- [Crear pedidos con idempotencia](/guias/pedidos)
- [Recibir webhooks firmados](/guias/webhooks)
- [Manejar errores y reintentos](/guias/errores)

Cuatro pasos, tres minutos, cero ambigüedad. Ese es el estándar.

  1. Registro de aplicaciones y credenciales OAuth

El flujo por el que un tercero se convierte en consumidor:

sequenceDiagram
    participant D as Desarrollador de CataBox
    participant P as Portal
    participant G as Gateway
    participant A as Servidor de autorización
    D->>P: Se registra y crea la aplicación CataBox
    D->>P: Declara URLs de retorno y ámbitos solicitados
    P->>A: Crea el cliente OAuth con esos datos
    A-->>P: client_id y client_secret si es confidencial
    P->>G: Da de alta el consumidor con su plan y cuota
    P-->>D: Muestra las credenciales, el secreto una sola vez
    D->>A: Solicita un token con Client Credentials o PKCE
    A-->>D: access_token con los ámbitos concedidos
    D->>G: GET /v1/cafes con el token
    G->>G: Valida firma, ámbito y cuota del consumidor
    G-->>D: 200 con el catálogo

Decisiones de diseño de este flujo, todas con consecuencias de seguridad (04-02 y 04-03):

  • El client_secret se muestra una sola vez. Se guarda como hash, igual que una contraseña. Si se pierde, se rota; no se recupera.
  • Aplicaciones públicas frente a confidenciales. Una SPA o una app móvil no pueden guardar un secreto: son públicas y deben usar Authorization Code con PKCE. El portal debe preguntar el tipo y no ofrecer secreto a las públicas.
  • Ámbitos mínimos. Que CataBox pida cafes.leer y resenas.escribir, no todos. El portal debe explicar qué permite cada ámbito con lenguaje claro, porque ese texto es el que verá el cliente final en la pantalla de consentimiento.
  • Ámbitos sensibles con revisión manual. resenas.moderar o envios.escribir no se conceden automáticamente: se solicitan y alguien los aprueba.
  • Aprobación separada para producción. El entorno de pruebas es inmediato; el acceso a producción exige revisar la aplicación. Es lo que evita que un desarrollador curioso llegue a datos reales.
  • Rotación de credenciales sin corte: poder tener dos secretos válidos a la vez durante la rotación. Sin eso, rotar implica un corte, y la consecuencia es que nadie rota nunca.

  1. El tiempo hasta la primera llamada con éxito

Hay una métrica que resume la calidad de la experiencia de integración: TTFHW (time to first hello world), el tiempo desde que alguien llega a tu portal hasta que recibe su primera respuesta 200.

Por qué importa tanto: en los primeros minutos, quien evalúa tu API decide si sigue o busca otra cosa. Un TTFHW de treinta minutos con tres correos a soporte por medio es una barrera comercial real, no un detalle de experiencia de usuario.

Tiempo Valoración Qué implica
< 5 min Excelente Registro automático, credenciales inmediatas, ejemplo copiable
5-15 min Bueno Algún paso manual o documentación algo dispersa
15-60 min Mejorable Documentación confusa o alta fricción en el registro
> 1 día Malo Aprobación manual, credenciales por correo, tickets

Cómo se mide de verdad: siéntate con alguien que no conozca la API, dale el enlace del portal y cronometra sin ayudarle. Anota cada punto donde duda, se equivoca o se para. Esos puntos son la lista de tareas, ordenada por impacto. Es una prueba que cuesta una hora y produce mejor información que cualquier encuesta.

Los frenos habituales, que se repiten en casi todas las APIs:

  1. El registro pide datos innecesarios en el primer paso (razón social, teléfono, dirección fiscal).
  2. Las credenciales requieren aprobación humana incluso para el entorno de pruebas.
  3. El ejemplo de la documentación no funciona copiado y pegado (falta una cabecera, la URL está desactualizada).
  4. El entorno de pruebas no tiene datos: el primer GET devuelve una lista vacía y parece que algo falla.
  5. Los errores no explican el problema: un 401 genérico sin decir si el token es inválido, ha caducado o le falta el ámbito.

El punto 4 es especialmente traicionero. El entorno de pruebas debe estar sembrado con datos ficticios ricos: nuestro npm run sembrar con caf_001, caf_002, cli_842 y ped_5001 cumple exactamente esa función, y por eso las primeras llamadas del portal devuelven algo interesante en lugar de {"datos": [], "total": 0}.

  1. Ciclo de vida e inventario de APIs

En 04-02, el noveno punto del OWASP API Top 10 era la gestión inadecuada del inventario: APIs olvidadas, versiones antiguas aún vivas, endpoints de pruebas expuestos. Aquí es donde se resuelve.

El gateway es la fuente de verdad del inventario, porque todo lo que se expone pasa por él. Si algo recibe tráfico externo y no está en la configuración del gateway, es una API en la sombra y hay un problema.

Cada API o versión tiene un ciclo de vida explícito:

Estado Significado Quién puede usarla Soporte
Diseño Contrato en revisión; mock disponible (05-04) Nadie, o pruebas internas
Beta Funciona, puede cambiar sin ciclo de deprecación Consumidores que aceptan el riesgo Sin garantías
Estable En producción, con garantías de compatibilidad Todos Completo
Obsoleta Funciona pero se retirará; Deprecation y Sunset (02-07) Los existentes; sin altas nuevas Correcciones de seguridad
Retirada Devuelve 410 version_api_retirada Nadie Ninguno
Zombi Nadie sabe que existe y sigue viva Cualquiera Ninguno

La última fila es el problema real. Las APIs zombis aparecen porque nadie tiene la lista completa, y sobreviven porque nadie se atreve a apagar algo por si acaso.

Las prácticas que lo evitan, todas apoyadas en piezas que ya tenemos:

  • Inventario automático desde la configuración del gateway, versionada en Git.
  • Métricas de uso por ruta, versión y consumidor (04-07 y el plugin prometheus con per_consumer). Antes de retirar /v1 la pregunta «¿quién la usa todavía?» tiene una respuesta con nombres y volúmenes, no una suposición.
  • Un dueño por API, con nombre y equipo. Sin dueño, no hay quien decida.
  • Fecha de revisión. Cada API se revisa al menos una vez al año: ¿sigue haciendo falta?, ¿está documentada?, ¿tiene consumidores?
  • Los entornos de no producción, cerrados. Preproducción sin autenticación, accesible desde internet, es un clásico y aparece en los informes de brechas con frecuencia deprimente.
  • La retirada, en dos fases. Primero el apagado de prueba: devolver 410 durante unas horas en una fecha anunciada. Los consumidores que quedaban aparecen inmediatamente. Después, la retirada definitiva. Es mucho más eficaz que cualquier correo de aviso.

  1. Monetización, como nota

Cuando la API es en sí misma un producto, el gateway y el portal aportan la infraestructura de cobro. Los modelos habituales:

Modelo Cómo funciona Ejemplo
Gratuito con límite Cuota generosa, gratis 1.000 llamadas al mes
Por niveles Planes con cuotas y funciones crecientes Gratis / Pro / Empresa
Por consumo Se paga por llamada o por unidad procesada 0,001 € por llamada
Por funciones Ciertos endpoints solo en planes altos Webhooks solo en Empresa
Reparto de ingresos El socio cobra una comisión por venta generada CataBox por pedido referido

Las piezas técnicas están todas en esta lección: cuotas en el gateway por consumidor, medición con las métricas por consumidor, planes en el portal, y la facturación integrada con la pasarela.

Un aviso sobre la medición, que es donde se cometen los errores caros: hay que decidir explícitamente si se cobran las llamadas fallidas. Cobrar un 500 propio es indefendible; no cobrar un 429 puede incentivar el abuso. Y la medición debe ser auditable: un cliente tiene derecho a ver su consumo desglosado y a cuadrarlo con su factura.

Para Tienda Aroma, la API no es el producto: es el canal. Un reparto de ingresos con CataBox por pedidos referidos tendría más sentido que cobrar por llamada.

Errores Comunes y Consejos

  • Poner un gateway teniendo una sola API. Añades un punto de fallo, latencia y una herramienta más que operar, a cambio de casi nada. El gateway se justifica con varios servicios o con terceros que gestionar.
  • Quitar la autenticación de la aplicación porque el gateway la hace. Si alguien alcanza el servicio esquivando el gateway, tu API está completamente abierta. Zero trust, siempre.
  • Delegar la autorización a nivel de recurso. El gateway no puede saber que ped_5001 es de cli_842. Es el fallo número uno del OWASP API Top 10 y no tiene solución fuera de la aplicación.
  • Meter lógica de negocio en el gateway. Reglas de dominio en un script Lua sin pruebas, sin tipos y sin revisión. Es el error más caro de revertir.
  • Configurar CORS en el gateway y en la aplicación con listas distintas. Un origen funciona en un entorno y falla en otro, y depurarlo lleva horas porque cada capa parece correcta. Una política, un dueño.
  • Olvidar exposed_headers en el gateway. El navegador recibe ETag y Link pero JavaScript no puede leerlos. Es el error de CORS más frustrante de 04-05.
  • trust proxy mal configurado. Con menos saltos de los que hay, la IP real se pierde; con más, un atacante puede falsificarla. El número debe coincidir exactamente.
  • Configurar el gateway por interfaz gráfica. Nadie recuerda qué se cambió ni por qué, y se pierde en la siguiente reconstrucción. Configuración declarativa versionada y aplicada desde la tubería.
  • No probar contra el gateway. El recorrido de Newman de 05-01 debe apuntar al gateway. Un strip_path equivocado o un CORS mal puesto se descubren en producción si no.
  • Un portal que es solo Swagger UI. La referencia sin guía de inicio, sin página de errores y sin ejemplos ejecutables deja a quien se integra buscando dónde empezar.
  • Aprobación manual para el entorno de pruebas. Multiplica el TTFHW por cien y hace que la gente pruebe la API de la competencia mientras espera.
  • Un entorno de pruebas vacío. La primera llamada devuelve {"datos": [], "total": 0} y parece rota. Siémbralo con datos ficticios ricos.
  • Consejo: mide el TTFHW con una persona real y un cronómetro. Una hora de observación produce mejor información que cualquier encuesta.
  • Consejo: antes de retirar una versión, haz un apagado de prueba. Unas horas de 410 en una fecha anunciada descubren a todos los consumidores que quedaban, cosa que ningún correo de aviso consigue.
  • Consejo: documenta en un ADR (04-01) qué política vive en el gateway y cuál en la aplicación. Es la información que falta el día del incidente.

Ejercicios

Ejercicio 1: repartir responsabilidades

Tienda Aroma va a poner Kong delante de la API. Para cada requisito, decide si se implementa en el gateway, en la aplicación o en ambos, y justifícalo:

  1. Rechazar peticiones sin token válido.
  2. Comprobar que un cliente solo ve sus propios pedidos.
  3. Limitar a CataBox a 10.000 llamadas al mes.
  4. Rechazar precioMin mayor que precioMax.
  5. Bloquear un rango de IP que está atacando el login.
  6. Devolver 409 pedido_ya_pagado si el pedido ya se pagó.
  7. Permitir peticiones desde https://panel.tiendaaroma.example y de ningún otro origen.
  8. Exigir la cabecera Idempotency-Key en POST /v1/pedidos.
  9. Rechazar cuerpos mayores de 100 KB.
  10. Devolver 304 cuando el ETag coincide con If-None-Match.

Ejercicio 2: la ruta de /v2 con convivencia

Tienda Aroma publica /v2, que vive en un servicio separado (api-v2.interno:3000), mientras /v1 sigue en el servicio actual con seis meses de convivencia. Escribe la configuración declarativa de Kong necesaria: los servicios y rutas, cómo se aplican los mismos plugins de rate limiting y CORS a ambas versiones sin duplicar configuración, y cómo se añaden las cabeceras Deprecation, Sunset y Link con la sucesora solo a las respuestas de /v1. Indica también qué pasa el día del Sunset.

Ejercicio 3: auditar un portal de desarrollador

Un portal de la competencia tiene: página de inicio con la propuesta de valor, referencia completa generada de OpenAPI con consola "probar ahora", formulario de contacto para solicitar acceso —respuesta en 2-3 días laborables—, PDF con los planes y precios, y una página de estado del servicio.

Identifica al menos cinco carencias graves, estima el TTFHW resultante justificando la estimación, y propón un plan priorizado de mejora con las tres primeras acciones, indicando qué métrica esperarías mover con cada una.

Soluciones

Solución 1

# Requisito Dónde Justificación
1 Rechazar peticiones sin token válido Ambos El gateway rechaza pronto y ahorra tráfico a la aplicación. La aplicación lo repite porque si alguien la alcanza esquivando el gateway —regla de red mal puesta, despliegue nuevo, atacante interno— sería la única defensa. Es el ejemplo canónico de defensa en profundidad.
2 Un cliente solo ve sus pedidos Solo la aplicación El gateway no puede consultar la base de datos para saber que ped_5001 es de cli_842. Delegarlo es imposible, e intentarlo con reglas en el gateway produciría una autorización incompleta y peligrosa (BOLA, 04-02).
3 10.000 llamadas al mes para CataBox Solo el gateway Es gestión de consumidores: cuota por plan y por ventana de tiempo. La aplicación no tiene ni debe tener el concepto de "plan de CataBox". Además, cambiar la cuota no debería exigir un despliegue.
4 precioMin > precioMax Solo la aplicación Es validación semántica: la relación entre dos campos. El gateway puede comprobar que ambos son números positivos con el JSON Schema; la relación entre ellos es lógica de dominio y su error (parametro_invalido con detalles) pertenece a tu catálogo.
5 Bloquear un rango de IP atacante Gateway (y CDN/WAF, mejor aún) Cuanto antes se corte el tráfico malicioso, menos recursos consume. Lo ideal es el WAF de la CDN, antes incluso del gateway. La aplicación no debería ver ese tráfico jamás.
6 409 pedido_ya_pagado Solo la aplicación Regla de negocio pura, dependiente del estado persistido. Ni siquiera es tentador delegarla.
7 CORS solo desde el panel Gateway, y en la aplicación con matices El gateway es el sitio natural y responde el preflight. Conviene mantener cors() en la aplicación con la misma lista para el desarrollo local sin gateway, pero documentando en un ADR que la fuente de verdad es el gateway para evitar la divergencia del apartado 11.
8 Idempotency-Key obligatoria Ambos, con reparto El gateway puede rechazar la petición si la cabecera falta (428), lo cual es barato y temprano. Pero la lógica real —guardar la clave, detectar el reintento, devolver la respuesta original, detectar la reutilización con distinto cuerpo (409)— requiere estado de negocio y se queda en la aplicación.
9 Cuerpos mayores de 100 KB Ambos El gateway lo corta antes de que el cuerpo llegue a tu proceso, que es lo eficiente. La aplicación mantiene express.json({limit: '100kb'}) como red de seguridad, con su error cuerpo_demasiado_grande.
10 304 con If-None-Match Solo la aplicación El ETag se calcula a partir del contenido del recurso y de su versión: solo la aplicación puede generarlo. Un gateway con caché puede servir respuestas ya cacheadas, pero la validación condicional que cierra el círculo con If-Match y el 412 de la concurrencia optimista (04-06) es de la aplicación.

Patrón que emerge: todo lo que depende del contenido o del estado persistido se queda en la aplicación; todo lo que depende de quién llama y cuánto llama va al gateway; y lo que es barato comprobar dos veces se hace en ambos.

Solución 2

# gateway/kong.yaml — convivencia de /v1 y /v2
_format_version: "3.0"

services:
  # ---- Servicio v1: el actual, en fase de deprecación ---------------------
  - name: api-v1
    url: http://api-tienda-aroma.interno:3000
    tags: ["api-publica", "obsoleta"]
    routes:
      - name: ruta-v1
        paths: ["/v1"]
        strip_path: false
        protocols: ["https"]
    plugins:
      # Cabeceras de deprecación SOLO en v1 (02-07).
      # Deprecation: fecha en que quedó obsoleta, como marca Unix (RFC 9745).
      # Sunset: fecha de retirada, en formato HTTP (RFC 8594).
      # Link con rel="successor-version": la alternativa, descubrible.
      - name: response-transformer
        config:
          add:
            headers:
              - "Deprecation: @1767225600"
              - "Sunset: Wed, 30 Jun 2027 23:59:59 GMT"
              - 'Link: <https://api.tiendaaroma.example/v2>; rel="successor-version"'
              - 'Warning: 299 - "La versión v1 se retirará el 2027-06-30. Migra a /v2: https://developers.tiendaaroma.example/migracion/v2"'

  # ---- Servicio v2: el nuevo, en otro despliegue --------------------------
  - name: api-v2
    url: http://api-v2.interno:3000
    tags: ["api-publica", "estable"]
    routes:
      - name: ruta-v2
        paths: ["/v2"]
        strip_path: false
        protocols: ["https"]

# ---------------------------------------------------------------------------
# PLUGINS GLOBALES: se aplican a AMBAS versiones sin duplicar configuración.
# Esta es la respuesta a "cómo no duplicar": los plugins sin `service` ni
# `route` son globales; solo se declaran por ruta los que difieren.
# ---------------------------------------------------------------------------
plugins:
  - name: rate-limiting
    config:
      minute: 600
      hour: 20000
      policy: redis
      redis: { host: redis.interno, port: 6379, database: 1 }
      limit_by: consumer
      fault_tolerant: true
      # La cuota es COMPARTIDA entre v1 y v2 a propósito: un cliente que migra
      # progresivamente no debe recibir el doble de cuota por usar ambas.

  - name: cors
    config:
      origins:
        - https://tiendaaroma.example
        - https://panel.tiendaaroma.example
      methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
      headers: [Authorization, Content-Type, Idempotency-Key, If-Match, If-None-Match]
      exposed_headers:
        - ETag
        - Link
        - Location
        - Retry-After
        - Deprecation          # imprescindible: sin esto la SPA no ve el aviso
        - Sunset
        - Aroma-Traza-Id
        - Aroma-RateLimit-Restantes
      credentials: true
      max_age: 3600

  - name: jwt-signer
    config:
      access_token_issuer: https://auth.tiendaaroma.example
      access_token_jwks_uri: https://auth.tiendaaroma.example/.well-known/jwks.json
      verify_access_token_signature: true
      verify_access_token_expiry: true

  - name: correlation-id
    config:
      header_name: Aroma-Traza-Id
      generator: uuid
      echo_downstream: true

  - name: prometheus
    config:
      per_consumer: true
      status_code_metrics: true
      latency_metrics: true

Cómo se evita la duplicación: los plugins declarados en el nivel superior (sin service ni route) son globales y se aplican a todas las rutas. Solo se declara por servicio o por ruta lo que difiere, que aquí es únicamente el response-transformer con las cabeceras de deprecación. Cambiar el límite global es una línea que afecta a ambas versiones.

Detalle que se olvida siempre: Deprecation y Sunset deben estar en exposed_headers del plugin de CORS. Si no, el navegador las recibe pero la SPA no puede leerlas, y la instrumentación que querías —que el front avise en consola de que usa una versión obsoleta— no funciona.

Qué pasa el día del Sunset (30 de junio de 2027):

  # Fase 1 — Apagado de prueba: unas horas, en una fecha anunciada con antelación.
  # Es lo que descubre a los consumidores que quedaban, cosa que los correos no logran.
  - name: request-termination
    route: ruta-v1
    config:
      status_code: 410
      content_type: "application/json"
      body: '{"error":{"codigo":"version_api_retirada","mensaje":"La versión v1 se retiró el 2027-06-30. Usa /v2: https://developers.tiendaaroma.example/migracion/v2","detalles":[]}}'

Secuencia recomendada:

  1. Meses antes: cabeceras Deprecation y Sunset activas (ya lo están), avisos en el changelog del portal, y correos a los consumidores identificados por las métricas del gateway, que dicen exactamente quién sigue llamando a /v1 y con qué volumen.
  2. Un mes antes: apagado de prueba de dos horas, anunciado. Aparecen los rezagados.
  3. El día del Sunset: se activa el request-termination con 410. 410 Gone y no 404, porque 410 significa "existió y se retiró deliberadamente", que es información útil para quien depura.
  4. Semanas después: se elimina la ruta de la configuración y se apaga el servicio api-v1. El 410 deja paso al 404 genérico.

Se mantiene el 410 explicativo durante semanas en lugar de borrar la ruta de inmediato porque un 404 seco deja al consumidor sin saber qué pasó, mientras que el 410 con enlace a la guía de migración es autoexplicativo.

Solución 3

Cinco carencias graves:

  1. Acceso con aprobación manual de 2-3 días. Es la carencia más grave con diferencia. Convierte una evaluación de cinco minutos en un proyecto de una semana, y quien evalúa alternativas probará la de la competencia mientras espera. Muchos nunca vuelven.
  2. No hay entorno de pruebas con credenciales inmediatas. Consecuencia de la anterior: no se puede tocar nada sin permiso, así que la consola "probar ahora" es decorativa. Nadie puede evaluar la API sin comprometerse antes.
  3. Solo hay referencia, no guías. La referencia dice qué campos acepta POST /pedidos; no explica la idempotencia, la paginación, cómo reintentar ante un 429 o cómo verificar la firma de un webhook. Sin eso, todas las integraciones se hacen mal de la misma manera y el coste se traslada a soporte.
  4. Precios en un PDF. Un PDF no se puede enlazar a una sección concreta, se desactualiza sin que nadie lo note, no es accesible y transmite que la información es estática. Además, si los planes están en PDF, las cuotas técnicas probablemente no estén documentadas en ninguna parte.
  5. No hay changelog ni política de deprecación. Es la carencia que más miedo da a quien va a construir un negocio encima: no hay forma de saber si la API cambiará ni con cuánto aviso. Sin un compromiso público de compatibilidad, integrarse es asumir un riesgo indefinido.

Carencias adicionales: no hay página de errores con el catálogo de códigos; no se ve una colección de Postman ni ejemplos ejecutables; nada indica los límites de uso técnicos; y no hay clientes generados ni SDK (05-02).

Estimación del TTFHW: 2-3 días laborables. El desglose justifica la cifra: descubrimiento y lectura, 10 minutos; rellenar el formulario, 5 minutos; espera de aprobación, 2-3 días; configurar la autenticación con documentación incompleta, 30-60 minutos; primera llamada con éxito, 10 minutos. El tiempo activo son unos 90 minutos; el tiempo transcurrido, tres días. Y lo que cuenta comercialmente es el transcurrido, porque es el que determina si la persona sigue interesada.

Plan priorizado — tres primeras acciones:

Acción 1 (impacto altísimo, coste medio): registro automático para el entorno de pruebas. Cualquiera se registra con un correo, crea una aplicación y obtiene client_id y client_secret de pruebas al instante. La aprobación manual se conserva solo para producción, que es donde de verdad hace falta. El sandbox debe estar sembrado con datos ficticios ricos para que la primera llamada devuelva algo interesante. Métrica esperada: TTFHW de 2-3 días a menos de 15 minutos. Es un cambio de orden de magnitud y, por sí solo, justifica el proyecto.

Acción 2 (impacto alto, coste bajo): guía de inicio de cuatro pasos y página de errores. Una página con registro → token → primera llamada → siguientes pasos, con comandos curl copiables y probados en CI para que nunca se desactualicen (05-04 ya nos enseñó a probar que los ejemplos del contrato funcionan). Y una página con el catálogo completo de errores: qué significa cada código, si conviene reintentar y qué hacer. Métrica esperada: reducción del tiempo activo de 90 a 20 minutos, y caída de los tickets de soporte de primer nivel, que suelen ser el 60-70 % del total y casi siempre son "no entiendo este error".

Acción 3 (impacto medio-alto, coste bajo): changelog público y compromiso de compatibilidad. Una página con el historial de cambios, un compromiso explícito —"no eliminamos campos sin seis meses de aviso; los cambios rompedores van en una versión mayor de la ruta"— y suscripción por correo o RSS. Es la pieza que convierte una API que se prueba en una API sobre la que alguien se atreve a construir un producto. Métrica esperada: conversión de cuentas de prueba a integraciones en producción. Y, a medio plazo, menos incidencias en cada despliegue, porque los consumidores se enteran de los cambios antes de sufrirlos.

Después: publicar los precios en HTML, ofrecer la colección de Postman con un botón de importar, generar SDK con OpenAPI Generator (05-02) y documentar los límites técnicos junto a los planes.

Conclusión

Has visto la capa que rodea a la API por fuera. Un API gateway centraliza las preocupaciones transversales del tráfico —rate limiting, terminación TLS, validación de JWT y OAuth por JWKS, CORS, compresión, enrutado por versión, cuotas por consumidor, métricas, mTLS con socios, listas negras— y las convierte en configuración declarativa versionable en lugar de código repetido en cada servicio. Tienes su configuración real para Tienda Aroma en gateway/kong.yaml, con los mismos límites de 04-04 y su fault_tolerant decidido ruta a ruta, la lista blanca de 04-05 con los exposed_headers que casi todo el mundo olvida, el correlation-id que propaga el Aroma-Traza-Id de 04-07 y el plugin de Prometheus segmentado por consumidor; y la alternativa equivalente en gateway/nginx.conf, con el X-Forwarded-For que da sentido al trust proxy 1 de la posición 1 de src/app.js.

Y tienes el criterio, que vale más que la configuración: el gateway sabe quién llama y a dónde; solo la aplicación sabe qué hay dentro y qué significa. Por eso el rate limiting, el TLS, la validación criptográfica del token y el CORS se delegan bien, mientras que la autorización a nivel de recurso, la lógica de negocio y la validación semántica no se delegan nunca —y por eso, con el gateway delante, de los dieciséis middlewares de src/app.js solo se retiran uno o dos—. El valor no está en adelgazar tu aplicación, sino en la consistencia entre varios servicios, en la gestión de consumidores que antes no existía y en poder cambiar políticas sin desplegar. Todo ello asumiendo sus riesgos con los ojos abiertos: punto único de fallo, latencia añadida, configuración divergente y la tentación siempre presente de meter reglas de negocio donde no hay pruebas ni revisión.

La segunda mitad de la lección era la otra cara: un portal de desarrollador con guía de inicio de cuatro pasos, referencia generada del openapi.yaml de 05-02, consola interactiva, registro de aplicaciones con credenciales OAuth inmediatas para el entorno de pruebas, página de errores con el catálogo de 02-04, changelog con los avisos de deprecación de 02-07, límites, estado del servicio y soporte. Con una métrica que lo resume todo, el tiempo hasta la primera llamada con éxito, que se mide con una persona real y un cronómetro y que decide si tu API se adopta o se abandona en la primera hora. Y con la gestión del ciclo de vida y del inventario, donde el gateway se convierte en la fuente de verdad de qué está expuesto y las métricas por consumidor responden con nombres y volúmenes a la pregunta «¿quién sigue usando /v1?» antes de retirarla —con un apagado de prueba de unas horas, que descubre rezagados como no lo consigue ningún correo—.

Con esto se cierra el módulo 5. La API de Tienda Aroma ya no está sola: tiene una colección de Postman ejecutable en CI, un contrato OpenAPI completo que sirve de documentación, de mock, de validador y de generador de clientes, la perspectiva para saber qué habría cambiado con otro framework y qué no, pruebas de contrato que impiden que el código y el contrato se separen junto a una puerta que detecta cambios rompedores, una tubería que construye, prueba y despliega sin cortes con migraciones retrocompatibles y vuelta atrás, y una capa de gateway y portal que la protege y la explica. Es una API operable y consumible, no solo correcta.

Lo que falta ya no es ninguna pieza suelta: es ver todo esto junto, aplicado de principio a fin y sostenido en el tiempo. En el módulo 6, Casos de Estudio y Proyectos, dejamos las herramientas y volvemos al diseño con todo lo aprendido encima: un caso de estudio completo de la API de una tienda en línea, recorriendo las decisiones desde los recursos hasta el despliegue (06-01); un segundo caso, el de una red social, donde los problemas son distintos —grafos de relaciones, líneas de tiempo, paginación por cursor a gran escala, contenido generado por usuarios y moderación— y obligan a replantear varias de las decisiones que aquí dimos por buenas (06-02); la evolución y el mantenimiento de una API en producción, es decir, qué ocurre los tres años siguientes al lanzamiento: deuda de contrato, migraciones de versión, incidentes reales y retirada de funcionalidades (06-03); y el proyecto final, en el que diseñarás y desarrollarás tu propia API RESTful aplicando los seis módulos completos (06-04).

Curso de REST API: Principios de Diseño y Desarrollo de APIs RESTful

Módulo 1: Introducción a las APIs RESTful

Módulo 2: Diseño de APIs RESTful

Módulo 3: Desarrollo de APIs RESTful

Módulo 4: Buenas Prácticas y Seguridad

Módulo 5: Herramientas y Frameworks

Módulo 6: Casos de Estudio y Proyectos

© Copyright 2026. Todos los derechos reservados