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
- Qué es un API gateway y qué problema resuelve
- Las funciones que asume, comparadas con el módulo 4
- Qué delegar y qué no delegar nunca
- La arquitectura completa: CDN, gateway, servicios
- El patrón backend for frontend
- El service mesh es otra cosa
- Los productos y cómo se comparan
- Configuración de Kong para Tienda Aroma
- La alternativa con NGINX
- Qué middlewares podríamos retirar y cuáles no
- Los riesgos del gateway
- Qué es un portal de desarrollador
- Qué contiene un buen portal
- Registro de aplicaciones y credenciales OAuth
- El tiempo hasta la primera llamada con éxito
- Ciclo de vida e inventario de APIs
- Monetización, como nota
- Balance del módulo 5
- 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.
- 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 | Sí |
| 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 | Sí |
| CORS | cors(opcionesCors) (04-05) |
Lista blanca declarativa, preflight | Sí |
| 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 | Sí |
| Enrutado y versionado | app.use('/v1', rutasV1) |
Ruta a servicio; /v1 y /v2 a destinos distintos |
Sí |
| 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 | Sí |
| 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 | Sí |
| Listas negras | No las hacemos | Bloqueo por IP, país, patrón o reputación | Sí |
| Reintentos y circuit breaker | Parcial (04-04) | Reintentos, timeouts y cortocircuito por destino | Sí |
| 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.
- 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 |
- 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.
- 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.
- 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.
- 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 | Sí | 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.
- 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: STRICTCuatro puntos de esa configuración que merecen comentario:
fault_tolerantdistinto según la ruta. En el límite global estrue: si Redis cae, preferimos dejar pasar tráfico a tirar la API entera. En el login esfalse: 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: consumerfrente alimit_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 recibeETagpero JavaScript no puede leerlo si no está enAccess-Control-Expose-Headers. Aquí queda declarado de una vez para toda la API.preflight_continue: false. El gateway responde losOPTIONSy 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.yamlEsto 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.
- 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.
- 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 |
Sí | 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:
- Consistencia entre varios servicios. Con cinco APIs, las políticas se escriben una vez en lugar de cinco.
- 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.
- 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.
- 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.
- 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.
- 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/pedidosaceptaIdempotency-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
429yRetry-After, con409de idempotencia, con412deIf-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.
curlcopiable, 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.
- 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_secretse 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.leeryresenas.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.moderaroenvios.escribirno 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.
- 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:
- El registro pide datos innecesarios en el primer paso (razón social, teléfono, dirección fiscal).
- Las credenciales requieren aprobación humana incluso para el entorno de pruebas.
- El ejemplo de la documentación no funciona copiado y pegado (falta una cabecera, la URL está desactualizada).
- El entorno de pruebas no tiene datos: el primer
GETdevuelve una lista vacía y parece que algo falla. - Los errores no explican el problema: un
401gené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}.
- 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
prometheusconper_consumer). Antes de retirar/v1la 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
410durante 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.
- 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_5001es decli_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_headersen el gateway. El navegador recibeETagyLinkpero JavaScript no puede leerlos. Es el error de CORS más frustrante de 04-05. trust proxymal 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_pathequivocado 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
410en 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:
- Rechazar peticiones sin token válido.
- Comprobar que un cliente solo ve sus propios pedidos.
- Limitar a CataBox a 10.000 llamadas al mes.
- Rechazar
precioMinmayor queprecioMax. - Bloquear un rango de IP que está atacando el login.
- Devolver
409 pedido_ya_pagadosi el pedido ya se pagó. - Permitir peticiones desde
https://panel.tiendaaroma.exampley de ningún otro origen. - Exigir la cabecera
Idempotency-KeyenPOST /v1/pedidos. - Rechazar cuerpos mayores de 100 KB.
- Devolver
304cuando elETagcoincide conIf-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: trueCó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:
- Meses antes: cabeceras
DeprecationySunsetactivas (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/v1y con qué volumen. - Un mes antes: apagado de prueba de dos horas, anunciado. Aparecen los rezagados.
- El día del
Sunset: se activa elrequest-terminationcon410.410 Goney no404, porque410significa "existió y se retiró deliberadamente", que es información útil para quien depura. - Semanas después: se elimina la ruta de la configuración y se apaga el servicio
api-v1. El410deja paso al404gené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:
- 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.
- 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.
- 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 un429o 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. - 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.
- 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
- ¿Qué es una API?
- Historia y evolución de las APIs
- Fundamentos de HTTP para APIs
- Principios básicos de REST
- Modelo de madurez de Richardson y HATEOAS
- REST vs. SOAP
- REST frente a GraphQL, gRPC y webhooks
Módulo 2: Diseño de APIs RESTful
- Principios de diseño de APIs RESTful
- Recursos y URIs
- Métodos HTTP
- Códigos de estado HTTP
- Representaciones, cabeceras y negociación de contenido
- Filtrado, ordenación, paginación y búsqueda
- Versionado de APIs
- Documentación de APIs
Módulo 3: Desarrollo de APIs RESTful
- Configuración del entorno de desarrollo
- Creación de un servidor básico
- Manejo de peticiones y respuestas
- Validación de datos de entrada
- Persistencia y capa de acceso a datos
- Autenticación y autorización
- Manejo de errores
- Pruebas y validación
Módulo 4: Buenas Prácticas y Seguridad
- Buenas prácticas en el diseño de APIs
- Seguridad en APIs RESTful
- OAuth 2.0 y OpenID Connect en la práctica
- Rate limiting y throttling
- CORS y políticas de seguridad
- Caché HTTP y rendimiento
- Observabilidad: logs, métricas y trazas
Módulo 5: Herramientas y Frameworks
- Postman para pruebas de APIs
- Swagger y OpenAPI para documentación
- Frameworks populares para APIs RESTful
- Contratos, mocks y pruebas automatizadas de API
- Integración continua y despliegue
- API gateways y portales de desarrollador
