En 06-02 comparamos el diseño de Tienda Aroma con el de CafeSocial y cerramos con una tesis incómoda: no existe un diseño REST universal. Ahora llega el último asunto, el que separa una API bien diseñada de una API que sigue viva a los tres años: qué ocurre después de publicarla. Porque el día del lanzamiento la API es tuya; a partir de la primera integración es de tus consumidores, y cada campo que devuelves se convierte en una promesa. Esta lección recorre los tres primeros años de la API de Tienda Aroma como una línea temporal con hitos concretos: el primer incidente, el post-mortem de la caída de Navidad, la deuda de contrato que se va acumulando, la migración a v2 con su calendario de doce meses y el trabajo silencioso de mantenimiento que nadie ve pero que sostiene todo lo demás.
Contenido
- La línea temporal de una API en producción
- El primer día: qué se vigila y qué es normal
- Gestión de incidentes: severidades, mitigación y comunicación
- El post-mortem sin culpables (documento completo)
- Escuchar a los consumidores
- La deuda de contrato
- Añadir sin romper: catálogo de cambios seguros
- Cuándo toca una
v2y cómo se migra en 12 meses - Retirar funcionalidades y el coste de lo que casi nadie usa
- Mantenimiento continuo: dependencias, secretos, SLO y coste
- Inventario de APIs y APIs zombis
- Gobierno con varios equipos
- Errores comunes, ejercicios y conclusión
- La línea temporal de una API en producción
Conviene ver el ciclo completo antes de entrar en cada hito. Estos son los momentos que marcaron los tres primeros años de https://api.tiendaaroma.example/v1.
graph LR
A["Mes 0<br/>Lanzamiento v1<br/>SPA + panel"] --> B["Mes 2<br/>Aroma Movil<br/>1er incidente S3"]
B --> C["Mes 5<br/>RapidoEnvios<br/>webhooks HMAC"]
C --> D["Mes 7<br/>Caida de Navidad<br/>34 min - post-mortem"]
D --> E["Mes 11<br/>Registro de deuda<br/>de contrato"]
E --> F["Ano 2 - mes 14<br/>CataBox OAuth<br/>+ analitica por cliente"]
F --> G["Ano 2 - mes 18<br/>Decision: v2"]
G --> H["Ano 2 - mes 20<br/>Anuncio + guia<br/>de migracion"]
H --> I["Ano 3 - mes 26<br/>Brownouts<br/>programados"]
I --> J["Ano 3 - mes 32<br/>Apagado v1<br/>410 retirada"]
Ninguno de estos hitos es un proyecto nuevo: todos son mantenimiento. Y el mantenimiento consume, en una API con varios consumidores externos, bastante más esfuerzo acumulado que la construcción inicial.
- El primer día: qué se vigila y qué es normal
El mes 0 la API se publica con dos consumidores propios: la SPA https://tiendaaroma.example y el panel interno. Todo el instrumental que montamos en 04-07 (pino para logs estructurados, prom-client para métricas, trazas con OpenTelemetry, /salud/vivo y /salud/listo) deja de ser un ejercicio y pasa a ser el único sitio donde se puede mirar.
Las cuatro señales que se vigilan desde el minuto uno son las clásicas de un servicio de petición-respuesta:
| Señal | Qué mide | Normal en el mes 0 | Alerta si |
|---|---|---|---|
| Tasa de error 5xx | error_interno, servicio_no_disponible |
< 0,1 % de las peticiones | > 0,5 % durante 5 min |
| Latencia p95 | Tiempo de respuesta por ruta | 120–180 ms en GET /cafes |
> 400 ms durante 10 min |
| Saturación | Conexiones a SQLite, memoria, event loop | Estable | Lag del event loop > 100 ms |
| Tráfico | Peticiones por minuto y por consumidor | Crece de forma suave | Salto x5 sin campaña conocida |
Y hay una quinta señal que casi nadie mira el primer día y que resulta ser la más informativa: la distribución de los 4xx.
# Top de errores de cliente de las ultimas 24 h, agrupados por codigo del catalogo
# (los logs de pino salen en JSON, asi que jq basta)
cat logs/api-*.log \
| jq -r 'select(.res.statusCode >= 400 and .res.statusCode < 500)
| "\(.res.statusCode) \(.error.codigo // "sin_codigo") \(.req.url | split("?")[0])"' \
| sort | uniq -c | sort -rn | head -20Salida real del segundo día:
412 400 parametro_invalido /v1/cafes
118 401 no_autenticado /v1/pedidos
97 404 cafe_no_encontrado /v1/cafes/{id}
31 409 conflicto_version /v1/cafes/{id}Qué es normal y qué no. Los 401 son normales: la SPA reintenta con el token caducado y renueva. Los 404 sobre /cafes/{id} también: hay enlaces antiguos indexados. Lo que no es normal son 412 parametro_invalido en GET /v1/cafes: eso no es un cliente torpe, es un contrato mal explicado. Al inspeccionar los detalles del error aparece el patrón: los clientes envían ?tueste=Medio con mayúscula inicial, y nuestro enumerado sólo acepta claro|medio|oscuro. La documentación lo decía; el mensaje de error, no. Se corrigió el mensaje —no la validación— y los 400 cayeron a 20 al día.
Regla del primer día: un error 4xx repetido no acusa al cliente, describe un defecto de diseño o de documentación de tu API.
El primer incidente (mes 2). Con el lanzamiento de Aroma Móvil aparecen los primeros 429. La app hace polling del carrito cada 5 segundos y el rate limiting de 04-04 empieza a devolver limite_peticiones con Retry-After. El diagnóstico tardó veinte minutos porque las cabeceras Aroma-RateLimit-Restantes no se estaban registrando en los logs. La solución no fue subir el límite: fue añadir ETag al carrito (ya lo teníamos en /cafes, ver 04-05) para que el polling respondiera 304 Not Modified a coste casi nulo, y publicar en la guía de integración que el intervalo recomendado era de 30 segundos.
- Gestión de incidentes: severidades, mitigación y comunicación
Detección: el presupuesto de error manda
En 04-07 fijamos un SLO de disponibilidad del 99,9 % mensual para las rutas de lectura. Ese 0,1 % son 43 minutos de presupuesto de error al mes. El presupuesto es lo que convierte una discusión de opiniones ("¿esto es grave?") en una decisión aritmética: si en tres días se ha consumido el 60 % del presupuesto mensual, se congelan los despliegues de funcionalidad y el equipo se dedica a fiabilidad hasta que la tasa se recupere.
Las alertas no se disparan sobre umbrales instantáneos, sino sobre velocidad de consumo del presupuesto (burn rate): consumir el presupuesto 14 veces más rápido de lo sostenible durante 5 minutos es un aviso inmediato; 6 veces más rápido durante una hora, un aviso normal.
Niveles de severidad
| Sev | Definición | Ejemplo en Tienda Aroma | Respuesta | Comunicación |
|---|---|---|---|---|
| S1 | API caída o compras imposibles para todos | POST /pedidos/{id}/pago devuelve 500 al 100 % |
Guardia inmediata, sala de incidente | Página de estado en < 15 min + aviso a socios |
| S2 | Degradación grave o funcionalidad crítica rota para un consumidor | Aroma Móvil recibe conflicto_version en cada actualización |
Guardia inmediata en horario extendido | Página de estado + correo al consumidor afectado |
| S3 | Degradación parcial sin pérdida de datos | p95 de /cafes a 900 ms; búsquedas lentas |
Siguiente día laborable | Nota en el portal de desarrollador |
| S4 | Defecto menor, contrato incumplido sin impacto | Retry-After ausente en un 429 concreto |
Backlog priorizado | Changelog |
Mitigar antes que diagnosticar
Es la regla más difícil de interiorizar para un equipo técnico, porque la curiosidad empuja hacia el por qué. Durante un incidente, el orden correcto es:
- Restaurar el servicio con lo que sea: revertir el último despliegue, apagar la feature flag (05-04), devolver el tráfico al color anterior en el blue-green, degradar una funcionalidad.
- Preservar evidencias: capturar trazas,
Aroma-Traza-Idde peticiones fallidas, salida deEXPLAIN, métricas del intervalo. - Después, entender la causa raíz con calma.
Un despliegue revertido en 4 minutos cuesta un post-mortem; un despliegue depurado en caliente durante 40 minutos cuesta el presupuesto de error del trimestre.
Qué se comunica a los consumidores
Con consumidores externos —RápidoEnvíos y CataBox—, callar es peor que equivocarse. El patrón que adoptamos: una entrada en la página de estado en menos de 15 minutos aunque no se sepa nada ("estamos investigando errores en la creación de pedidos"), actualizaciones cada 30 minutos, y una nota final con el enlace al post-mortem cuando esté publicado. Nada de detalles internos de infraestructura; sí el impacto observable y el consejo de reintento.
- El post-mortem sin culpables
Mes 7, 18 de diciembre. La campaña de Navidad multiplica el tráfico por seis. Dos semanas antes se había añadido el filtro ?origen= a GET /v1/cafes —un cambio retrocompatible de manual, tres líneas de código— pero sin índice en la columna correspondiente. Con el catálogo crecido y el tráfico de campaña, la base de datos se satura y la API queda inaccesible 34 minutos.
Este es el documento tal como quedó en docs/incidentes/. Fíjate en lo que no contiene: ningún nombre asociado a la causa.
# Post-mortem INC-041: saturacion de la base de datos por filtro sin indice
- **Estado:** cerrado
- **Severidad:** S1
- **Fecha:** 18 de diciembre, 19:42 - 20:16 (CET)
- **Duracion del impacto:** 34 minutos
- **Autor:** equipo de plataforma de API
- **Revisado por:** producto, soporte, seguridad
## Resumen
Un filtro anadido dos semanas antes (`GET /v1/cafes?origen=`) provocaba un escaneo
completo de la tabla `cafes`. Con el trafico de la campana de Navidad (x6 sobre la
media) las consultas agotaron el pool de conexiones y toda la API dejo de responder,
incluidas rutas que no usaban ese filtro.
## Cronologia (hora CET)
| Hora | Evento |
|---|---|
| 04/12 10:15 | Se despliega el filtro `origen` en `/v1/cafes`. Sin indice. Sin carga de prueba. |
| 18/12 19:31 | Empieza el envio de la newsletter de Navidad con enlaces a `?origen=etiopia`. |
| 18/12 19:38 | p95 de `/v1/cafes` pasa de 160 ms a 2,4 s. Nadie mira. |
| 18/12 19:42 | Alerta de burn rate 14x. Comienza el impacto medible. |
| 18/12 19:44 | La guardia acusa recibo. Se abre la sala de incidente. |
| 18/12 19:47 | Se publica la primera nota en la pagina de estado ("investigando"). |
| 18/12 19:53 | Se descarta el ultimo despliegue (del 17/12) como causa: revertirlo no cambia nada. |
| 18/12 20:01 | Se identifica en las trazas que el 88 % del tiempo se va en una unica consulta. |
| 18/12 20:04 | **Mitigacion:** se desactiva el filtro `origen` con la feature flag y se devuelve
`400 parametro_invalido` temporalmente para ese parametro. |
| 18/12 20:09 | La latencia p95 vuelve a 210 ms. El pool se recupera. |
| 18/12 20:16 | Fin del impacto. Nota de resolucion en la pagina de estado. |
| 18/12 21:30 | Se crea el indice en una ventana de baja carga y se reactiva el filtro. |
## Impacto medido
- 34 minutos de indisponibilidad parcial-total (79 % de las peticiones con 5xx o timeout).
- 41.200 peticiones fallidas; 218 intentos de `POST /v1/pedidos` sin completar.
- 96 pedidos no cerrados durante la ventana; 61 se recuperaron solos por reintento
del cliente gracias a `Idempotency-Key` (no hubo cobros duplicados).
- Presupuesto de error mensual consumido: 79 % (34 min sobre 43 min disponibles).
- 7 tickets de soporte y 1 aviso de RapidoEnvios por webhooks de envio retrasados.
## Causa raiz
La consulta generada por el filtro `origen` no disponia de indice y realizaba un
escaneo secuencial de `cafes`. Bajo concurrencia alta, cada consulta mantenia su
conexion ocupada el tiempo suficiente para agotar el pool, de modo que peticiones
ajenas al filtro (`/v1/pedidos`, `/v1/clientes`) tambien quedaban en espera.
Causa contribuyente: la revision del cambio se centro en el contrato (nombre del
parametro, validacion, documentacion en `openapi.yaml`) y no en su plan de ejecucion.
No existia ninguna comprobacion automatica que lo exigiese.
## Que fallo en la deteccion
- La degradacion empezo a las 19:38 y la alerta salto a las 19:42: cuatro minutos
perdidos porque la alerta de latencia solo miraba el agregado global, no por ruta.
- No habia alerta sobre saturacion del pool de conexiones, que era la senal mas
temprana y la mas inequivoca.
- El envio de la newsletter no estaba anunciado al equipo de plataforma.
## Que hicimos bien
- La feature flag permitio mitigar sin desplegar codigo.
- La idempotencia evito cobros duplicados en los reintentos.
- La pagina de estado se actualizo antes de tener diagnostico.
## Acciones
| # | Accion | Tipo | Responsable | Plazo |
|---|---|---|---|---|
| 1 | Crear indice `idx_cafes_origen` y validar con EXPLAIN QUERY PLAN | Correctiva | Equipo datos | 19/12 (hecho) |
| 2 | Alerta de latencia p95 **por ruta**, no solo agregada | Deteccion | Plataforma | 09/01 |
| 3 | Alerta de saturacion del pool de conexiones al 80 % | Deteccion | Plataforma | 09/01 |
| 4 | Anadir a la checklist de revision: "todo filtro nuevo declara su indice" | Preventiva | Gobierno API | 15/01 |
| 5 | Prueba de carga automatica en CI para rutas de listado | Preventiva | Plataforma | 31/01 |
| 6 | Calendario compartido de campanas de marketing con plataforma | Organizativa | Producto | 15/01 |
| 7 | Publicar resumen del incidente en el portal de desarrollador | Comunicacion | Soporte | 22/12 (hecho) |
## Que NO es una accion
"Tener mas cuidado al revisar" no es una accion: no es verificable ni deja rastro.
Si una accion no se puede cerrar con un enlace a un commit, a un tablero o a un
documento, no entra en esta tabla.La verificación de la acción 1, para que quede claro qué se comprueba:
-- Antes: escaneo completo de la tabla
EXPLAIN QUERY PLAN
SELECT * FROM cafes WHERE origen = 'etiopia' ORDER BY nombre LIMIT 20;
-- SCAN cafes
CREATE INDEX idx_cafes_origen ON cafes (origen, nombre);
-- Despues: busqueda por indice
EXPLAIN QUERY PLAN
SELECT * FROM cafes WHERE origen = 'etiopia' ORDER BY nombre LIMIT 20;
-- SEARCH cafes USING INDEX idx_cafes_origen (origen=?)Sin culpables no significa sin responsables. Las acciones tienen dueño y plazo; la causa no tiene dueño. La persona que añadió el filtro sin índice hizo lo que el sistema le permitía hacer: no había checklist, ni prueba de carga, ni alerta. El sistema falló, y el sistema es lo que se arregla.
- Escuchar a los consumidores
A partir del mes 9, la pregunta deja de ser "¿funciona?" y pasa a ser "¿qué están haciendo realmente con ella?". Tres fuentes:
Analítica de uso por endpoint y por cliente. Cada token JWT lleva un client_id (04-02). Se etiqueta la métrica de peticiones con ese identificador, pero con cuidado con la cardinalidad: etiquetar por ruta con parámetros (/v1/cafes/caf_001) genera una serie temporal por café y hace estallar la memoria de Prometheus. Se etiqueta por plantilla de ruta y por cliente, que son conjuntos pequeños y acotados.
// metricas/peticiones.js — cardinalidad controlada a proposito
import client from 'prom-client';
const peticiones = new client.Counter({
name: 'aroma_peticiones_total',
help: 'Peticiones atendidas por la API',
// ruta = PLANTILLA (/v1/cafes/:id), nunca la ruta concreta.
// cliente = client_id del JWT, un conjunto cerrado de ~6 consumidores.
// version = v1 | v2, imprescindible para la migracion (apartado 8).
labelNames: ['metodo', 'ruta', 'codigo', 'cliente', 'version'],
});
export function contarPeticion(req, res) {
peticiones.inc({
metodo: req.method,
ruta: req.route?.path ?? 'desconocida', // plantilla, no URL real
codigo: res.statusCode,
cliente: req.auth?.clientId ?? 'anonimo', // etiqueta acotada
version: req.baseUrl.startsWith('/v2') ? 'v2' : 'v1',
});
}Qué campos no usa nadie. Una API REST devuelve la representación completa, así que no sabes qué lee el cliente… salvo que se lo preguntes. Dos técnicas baratas: (a) medir el uso del parámetro de proyección si lo tienes (?campos=), y (b) preguntar directamente en la encuesta anual a integradores. En Tienda Aroma descubrimos así que notasCata sólo lo consumían la SPA y CataBox, y que el campo _links.self de cada elemento de colección no lo usaba absolutamente nadie: los clientes construían las URLs por concatenación. Dato incómodo que se apuntó en la autocrítica de HATEOAS de 06-01.
Errores 4xx recurrentes por cliente. Es la mejor lista de tareas de documentación que existe:
| Error repetido | Cliente | Interpretación real | Acción tomada |
|---|---|---|---|
parametro_invalido en ordenar |
CataBox | El separador -precioEuros no estaba en los ejemplos |
Ejemplo añadido en openapi.yaml |
conflicto_version en PUT /cafes/{id} |
Panel interno | No reenviaba el ETag tras un fallo |
Nota en la guía + detalles más explícitos |
stock_insuficiente en pago |
Aroma Móvil | Aviso de stock demasiado tardío | Deuda de diseño (ver apartado 6) |
no_autenticado masivo a las 03:00 |
RápidoEnvíos | Renovación de token mal programada | Correo directo al socio |
Y el canal de soporte: una dirección [email protected] que llega a un humano, con el compromiso público de responder en 2 días laborables. Cada ticket se etiqueta como defecto, documentación o petición de funcionalidad; los de documentación se cierran editando el openapi.yaml, nunca respondiendo sólo por correo.
- La deuda de contrato
Mes 11. El equipo hace una lista de "cosas que arreglaríamos si empezáramos hoy" y se da cuenta de que ninguna se puede arreglar. Eso es deuda de contrato: decisiones publicadas que ya no son las mejores, pero que sostienen a consumidores reales.
Se acumula por tres motivos, y ninguno es negligencia:
- El dominio cambia. Cuando se diseñó
tuestecon tres valores no existía el tueste filtro en el catálogo. - El estándar cambia o se conoce mejor.
application/problem+json(RFC 9457) existía, pero se optó por un formato propio; hoy sería la opción obvia. - Aciertas al 80 %. El envoltorio
{"datos": [...], "total": n}funcionó bien, pero encarece cada respuesta y confunde a los clientes que esperan un array desnudo.
En 06-01 ya criticamos cuatro decisiones. Ahora tienen precio:
| Deuda | Cambio deseado | Por qué no se puede hacer en v1 |
Coste de convivir |
|---|---|---|---|
| Nombre poco claro | notasCata → notasDeCata |
Rompe a los 5 consumidores | Bajo: confunde a los nuevos |
| Envoltorio | Quitar datos / usar sólo Link |
Rompe todo el parseo de colecciones | Medio: código duplicado en clientes |
| Formato de error | Adoptar problem+json |
Cambia el Content-Type y la forma del cuerpo |
Medio: fricción con librerías estándar |
| Reseñas con dos rutas | Dejar sólo /cafes/{id}/resenas |
La SPA usa /resenas?cafeId= |
Alto: dos caminos que mantener y cachear |
| Aviso tardío de stock | Validar stock al añadir al carrito | Cambia la semántica de POST /carritos/{id}/items |
Alto: soporte recurrente |
El artefacto que hace que esto no se olvide es el registro de deuda de contrato, versionado junto al código en docs/deuda-contrato.yaml. Es un documento vivo: se revisa cada trimestre y es la materia prima de la decisión sobre v2.
# docs/deuda-contrato.yaml — se revisa cada trimestre
deudas:
- id: DC-004
titulo: "El aviso de stock insuficiente llega en el pago, no en el carrito"
origen: "Post-mortem INC-041 y 38 tickets de soporte"
consumidores_afectados: [spa-tienda, aroma-movil]
impacto: alto
solucion_deseada: >
Validar stock en POST /carritos/{id}/items y devolver 409 stock_insuficiente
en ese momento, manteniendo la validacion final en el pago.
rompe_contrato: true # cambia el codigo de estado en un caso antes valido
candidata_v2: true
creada: "ano 1, mes 11"
revisada: "ano 2, mes 18"
- id: DC-007
titulo: "Formato de error propio en lugar de application/problem+json"
consumidores_afectados: [todos]
impacto: medio
rompe_contrato: true
candidata_v2: true
nota: >
Mitigacion posible sin romper: negociacion de contenido. Si el cliente envia
Accept: application/problem+json devolvemos ese formato; si no, el propio.Fíjate en la nota de DC-007: parte de la deuda se puede pagar sin romper nada si se piensa en términos de negociación de contenido (02-05). No toda deuda exige una versión nueva.
- Añadir sin romper: catálogo de cambios seguros
Antes de plantearse una v2, hay que agotar lo que se puede hacer dentro de v1. La regla general la vimos en 02-07: añadir es seguro, quitar y cambiar el significado no lo es. El detalle importa mucho más de lo que parece.
| Cambio | ¿Rompe? | Solución retrocompatible |
|---|---|---|
Nuevo campo origenCertificado en Cafe |
No* | Añadirlo opcional y documentarlo; los clientes que no lo conocen lo ignoran |
Nuevo endpoint /cafes/{id}/lotes |
No | Publicar y documentar |
Nuevo filtro ?certificado=true |
No | Opcional, con valor por defecto = comportamiento actual |
Nuevo valor filtro en el enumerado tueste (respuesta) |
Sí, en la práctica | Introducir con aviso previo; los clientes con switch exhaustivo fallan |
Nuevo valor aceptado en tueste (petición) |
No | Ampliar la validación de entrada es seguro |
| Endurecer una validación existente | Sí | Fase de aviso: registrar, no rechazar; luego rechazar |
Cambiar el valor por defecto de limite de 20 a 50 |
Sí | No cambiarlo; o cambiarlo sólo para clientes nuevos |
Renombrar notasCata |
Sí | Duplicar campo + deprecar el viejo, o esperar a v2 |
| Quitar un campo | Sí | Sólo en v2 |
Cambiar 200 por 202 en una operación |
Sí | Sólo en v2 |
| Hacer obligatorio un campo de entrada opcional | Sí | Sólo en v2 |
* El asterisco del campo nuevo. "Añadir un campo no rompe nada" sólo es cierto si los clientes son tolerant readers: si ignoran lo que no conocen. Un cliente que valide la respuesta contra un esquema estricto con additionalProperties: false, o que use un lenguaje que falle al deserializar campos desconocidos, se rompe con un campo nuevo. Por eso la guía de integración de Tienda Aroma dice esto en la primera página:
// Contrato de tolerancia publicado en la guia de integracion.
// Asi debe leer un cliente la respuesta de GET /v1/cafes/caf_001
const cafe = await respuesta.json();
// BIEN: se leen los campos conocidos y se ignora el resto.
const vista = {
id: cafe.id,
nombre: cafe.nombre,
precioEuros: cafe.precioEuros,
// Valor desconocido en un enumerado: se degrada, no se revienta.
tueste: ['claro', 'medio', 'oscuro'].includes(cafe.tueste) ? cafe.tueste : 'otro',
};
// MAL: rompe en cuanto anadimos origenCertificado.
// const { id, nombre, precioEuros, ...resto } = cafe;
// if (Object.keys(resto).length > 0) throw new Error('Campo desconocido');El caso peligroso: endurecer una validación. En el mes 14 se descubrió que notasCata admitía textos de cualquier longitud y alguien había guardado 40 KB. La tentación es añadir .max(500) en el esquema Zod (03-02) y desplegar. Eso convierte peticiones antes válidas en 422 datos_invalidos: es un cambio rompedor aunque no toque ni un nombre de campo. El procedimiento correcto es en dos tiempos.
// Fase 1 (semanas 1-6): modo aviso. No se rechaza nada, se mide quien incumpliria.
const esquemaCafe = z.object({
nombre: z.string().min(1).max(120),
notasCata: z.string(), // sin limite todavia
// ...
}).superRefine((datos, ctx) => {
if (datos.notasCata.length > 500) {
// Se registra con el cliente para poder avisarle uno a uno.
log.warn({
evento: 'validacion_futura_incumplida',
regla: 'notasCata_max_500',
longitud: datos.notasCata.length,
cliente: ctx.path,
}, 'Peticion que sera rechazada a partir del 1 de marzo');
contadorAvisos.inc({ regla: 'notasCata_max_500' });
}
});
// Fase 2 (semana 7, solo si el contador esta a cero durante 14 dias):
// notasCata: z.string().max(500)Si al cabo de seis semanas el contador sigue subiendo, no se despliega la fase 2: se llama al cliente. La fecha se mueve; el contrato no se rompe por sorpresa.
- Cuándo toca una
v2 y cómo se migra en 12 meses
v2 y cómo se migra en 12 mesesLa respuesta correcta casi siempre es "todavía no"
Una v2 no es un logro, es una factura: dos bases de código o dos capas de traducción, dos juegos de pruebas, dos documentaciones, dos versiones del openapi.yaml, y consumidores que tardarán meses en moverse. Criterios honestos:
Señales de que NO toca v2:
- El cambio se puede hacer añadiendo (apartado 7).
- Molesta al equipo pero no a los consumidores (
notasCatapor sí solo no justifica nada). - Es un problema de documentación disfrazado de problema de diseño.
- Hay menos de tres deudas de impacto alto acumuladas.
Señales de que SÍ toca:
- Varias deudas de impacto alto que sólo se resuelven rompiendo, y que causan incidentes o soporte recurrente.
- El modelo de dominio ha cambiado de verdad (Tienda Aroma pasó a vender suscripciones, y un pedido recurrente no encaja en el recurso
/pedidosactual). - Cambios de seguridad no negociables.
- El coste de mantener los apaños supera el coste de migrar.
Mes 18. Tienda Aroma decide la v2 con cuatro deudas de impacto alto y un dominio nuevo (suscripciones). Se escribe un ADR en docs/decisiones/0031-lanzar-v2.md con la alternativa descartada (seguir parcheando v1 con problem+json negociado y un recurso /suscripciones colgado de v1) y por qué se rechaza.
Calendario de 12 meses
Recordemos la política publicada: versionado sólo en la ruta, y v1 y v2 conviven al menos 6 meses. En la práctica, para una API con socios externos, seis meses es el mínimo legal y doce el mínimo razonable.
| Mes | Hito | Qué ve el consumidor |
|---|---|---|
| M0 | Anuncio + guía de migración + v2 en beta |
Correo, portal, changelog, entorno de pruebas |
| M1 | v2 estable en producción |
Ambas versiones funcionando |
| M1 | v1 marcada como deprecada |
Cabeceras Deprecation, Sunset, Link rel="successor-version" |
| M2–M5 | Acompañamiento | Soporte prioritario a integradores, ejemplos, sesiones técnicas |
| M6 | Primer corte de métricas | Informe interno: quién sigue en v1 |
| M7 | Contacto directo con rezagados | Llamada, no correo automático |
| M9 | Brownout 1: 30 min de 410 en v1 |
Aviso 2 semanas antes; ventana de baja carga |
| M10 | Brownout 2: 2 h de 410 |
Aviso 2 semanas antes |
| M11 | Brownout 3: 8 h de 410 |
Aviso 2 semanas antes |
| M12 | Apagado definitivo de v1 |
410 Gone permanente con version_api_retirada |
| M12+3 | Borrado del código de v1 |
— |
Guía de migración: la tabla de equivalencias
Es el documento que decide si la migración es de dos días o de dos meses para el consumidor. Nada de prosa: equivalencias literales.
v1 |
v2 |
Nota |
|---|---|---|
GET /v1/cafes → {"datos":[…],"total":n} |
GET /v2/cafes → {"items":[…],"paginacion":{…}} |
El total exacto pasa a ser opcional |
notasCata |
notasDeCata |
Renombrado |
tueste: claro|medio|oscuro |
tueste: claro|medio|oscuro|filtro |
Valor nuevo |
Error propio {"error":{…}} |
application/problem+json |
codigo → type (URI del catálogo) |
GET /v1/resenas?cafeId= |
GET /v2/cafes/{id}/resenas |
Ruta única |
POST /v1/carritos/{id}/items (sin control de stock) |
Igual, pero puede devolver 409 stock_insuficiente |
Aviso temprano |
| — | GET /v2/suscripciones |
Recurso nuevo |
?desplazamiento= |
?cursor= |
desplazamiento se acepta 6 meses en v2 |
Las cabeceras de deprecación en funcionamiento
Retomando 02-07, así responde v1 a partir del mes 1:
GET /v1/cafes?origen=etiopia HTTP/1.1
Host: api.tiendaaroma.example
Authorization: Bearer eyJhbGciOi...
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Deprecation: @1836345600
Sunset: Sat, 12 Jun 2027 00:00:00 GMT
Link: <https://api.tiendaaroma.example/v2/cafes?origen=etiopia>; rel="successor-version",
<https://docs.tiendaaroma.example/migracion-v2>; rel="deprecation"; type="text/html"
Aroma-Traza-Id: trz_9f2a41c8// middleware/deprecacion.js — se aplica a todo el router de /v1
const SUNSET = new Date('2027-06-12T00:00:00Z');
const DEPRECATION_UNIX = Math.floor(new Date('2026-06-12T00:00:00Z').getTime() / 1000);
export function deprecarV1(req, res, siguiente) {
// Deprecation: cuando se DECLARO obsoleta (formato IMF-fecha o marca @unix).
res.set('Deprecation', `@${DEPRECATION_UNIX}`);
// Sunset: cuando dejara de responder. Es una promesa; no se adelanta jamas.
res.set('Sunset', SUNSET.toUTCString());
res.append('Link',
`<https://api.tiendaaroma.example/v2${req.path}>; rel="successor-version"`);
res.append('Link',
'<https://docs.tiendaaroma.example/migracion-v2>; rel="deprecation"; type="text/html"');
siguiente();
}Saber quién sigue en v1
Con la etiqueta version del contador del apartado 5, la pregunta se responde sola:
# Consumidores que aun usan v1 en los ultimos 7 dias, ordenados por volumen
curl -sG "http://prometheus.interno:9090/api/v1/query" \
--data-urlencode 'query=topk(10, sum by (cliente) (increase(aroma_peticiones_total{version="v1"}[7d])))' \
| jq -r '.data.result[] | "\(.metric.cliente)\t\(.value[1] | tonumber | floor)"'catabox 412803 integracion-erp 18744 <- nadie sabia que existia (ver apartado 11) aroma-movil 2210 <- version antigua de la app, sin actualizar
Ese segundo resultado es el motivo por el que no se apaga una versión por calendario sin mirar las métricas: aparece siempre un consumidor olvidado. Y el tercero recuerda que en las apps móviles no controlas cuándo actualiza el usuario: la app antigua seguirá viva durante meses.
Brownouts
Un brownout es un apagado breve y anunciado de v1: durante la ventana, todas las peticiones reciben 410. Su función no es técnica, es psicológica: convierte una fecha lejana en un incidente real en el entorno del consumidor, que es lo único que mueve prioridades.
// middleware/brownout.js — apagados breves programados de v1
const VENTANAS = [
{ desde: '2027-03-10T09:00:00Z', hasta: '2027-03-10T09:30:00Z' }, // 30 min
{ desde: '2027-04-14T09:00:00Z', hasta: '2027-04-14T11:00:00Z' }, // 2 h
{ desde: '2027-05-12T07:00:00Z', hasta: '2027-05-12T15:00:00Z' }, // 8 h
];
export function brownoutV1(req, res, siguiente) {
const ahora = Date.now();
const ventana = VENTANAS.find(v =>
ahora >= Date.parse(v.desde) && ahora < Date.parse(v.hasta));
if (!ventana) return siguiente();
// Retry-After indica cuando vuelve v1: durante el brownout SI vuelve.
res.set('Retry-After', String(Math.ceil((Date.parse(ventana.hasta) - ahora) / 1000)));
res.status(410).json({
error: {
codigo: 'version_api_retirada',
mensaje: 'Apagado programado de /v1. Migra a /v2 antes del 12/06/2027.',
detalles: [{ campo: 'version', valor: 'v1', sucesor: '/v2' }],
},
});
}Y el apagado definitivo, ya sin Retry-After:
GET /v1/cafes HTTP/1.1
Host: api.tiendaaroma.example
HTTP/1.1 410 Gone
Content-Type: application/json; charset=utf-8
Link: <https://api.tiendaaroma.example/v2/cafes>; rel="successor-version"
{
"error": {
"codigo": "version_api_retirada",
"mensaje": "La version v1 se retiro el 12/06/2027. Usa /v2.",
"detalles": [
{ "campo": "version", "valor": "v1" },
{ "campo": "guia", "valor": "https://docs.tiendaaroma.example/migracion-v2" }
]
}
}410 Gone y no 404: la diferencia comunica que el recurso existió y desapareció a propósito, y evita que un cliente crea que se ha equivocado de ruta.
- Retirar funcionalidades y el coste de lo que casi nadie usa
Año 2. El endpoint GET /v1/cafes/{id}/maridajes, añadido en el mes 4 por petición de marketing, recibe 40 peticiones al mes de un único cliente. Su coste no es cero:
- Aparece en
openapi.yaml, así que hay que documentarlo y revisarlo con Spectral. - Tiene pruebas que se ejecutan en cada CI y que a veces fallan por datos de prueba.
- Tiene una tabla con su migración, que hay que arrastrar en cada cambio de esquema.
- Bloquea decisiones: cualquier refactor del recurso
Cafedebe contemplarlo. - Ocupa espacio mental en cada revisión de diseño.
La retirada de una funcionalidad concreta sigue el mismo protocolo que una versión, en pequeño: anuncio, Deprecation/Sunset sólo en esa ruta, contacto con el único consumidor, y 410. Duró tres meses. Lo que no se debe hacer nunca es borrarlo porque "casi nadie lo usa": ese "casi" es una empresa que depende de ello.
- Mantenimiento continuo: lo que nadie ve
Trabajo recurrente que no produce funcionalidades y sin el cual la API se degrada sola.
Dependencias y CVE. Auditoría semanal automatizada en el ci.yml de 05-04:
npm audit --audit-level=high # falla el pipeline si hay alta o critica
npm outdated # informe semanal, no bloqueante
docker scout cves tiendaaroma-api:latestLa política acordada: vulnerabilidad crítica en dependencia explotable, parche en 48 h; alta, en 7 días; el resto, en la ventana mensual de mantenimiento.
Versión de Node. Node 20 entra en fin de soporte y hay que saltar a la siguiente LTS. Es un cambio invisible para el consumidor y peligroso para ti: se hace por canary (05-04), con el 5 % del tráfico durante 48 h, comparando p95 y tasa de error entre ambos grupos. Se apunta en el calendario antes de que el soporte expire, no después.
Rotación de secretos y claves de firma. Tres relojes distintos:
| Secreto | Rotación | Cómo se rota sin cortar |
|---|---|---|
| Clave de firma JWT | Cada 90 días | Dos claves activas con kid; se firma con la nueva, se verifican ambas |
| Secreto HMAC de webhooks a RápidoEnvíos | Cada 180 días | Firma doble durante 14 días (Aroma-Firma y Aroma-Firma-Siguiente) |
| Credenciales de OAuth de CataBox | Anual o ante incidente | Solapamiento de 30 días |
Revisión de SLO. Cada semestre. Si el SLO del 99,9 % nunca se ha rozado en un año, o está mal medido o es demasiado laxo. Si se incumple cada mes, o no es alcanzable con la arquitectura actual, o el presupuesto de error se está usando como excusa. En Tienda Aroma, el SLO de latencia de /cafes se endureció de 400 ms a 300 ms en el año 2 tras el trabajo de índices.
Revisión de seguridad. Repaso anual del OWASP API Top 10 (04-01) contra el estado real: autorización a nivel de objeto en cada endpoint nuevo, límites de consumo, exposición de datos. Se documenta como cualquier revisión de diseño.
El coste de infraestructura como señal de diseño. La factura de la API es una métrica de diseño disfrazada de métrica financiera. Si un endpoint cuesta desproporcionadamente, casi siempre hay una decisión de contrato detrás: GET /v1/pedidos sin paginación obligatoria devolviendo miles de elementos, ausencia de ETag en un recurso muy consultado, o un cliente haciendo polling donde debería haber un webhook. Antes de escalar la infraestructura, revisa el contrato: sale más barato.
- Inventario de APIs y APIs zombis
En 04-02 definimos la propiedad de cada API y en 05-06 montamos el portal de desarrollador con su inventario. Su utilidad real aparece justo ahora: en la migración a v2 apareció un consumidor llamado integracion-erp que nadie recordaba haber autorizado. Existía, funcionaba y estaba en producción.
Una API zombi es una API (o una versión, o un endpoint) que sigue respondiendo, consume recursos y presenta superficie de ataque, pero no tiene dueño identificable. Se detectan cruzando tres fuentes: el inventario declarado, las métricas reales de tráfico y el registro de credenciales emitidas. Cualquier fila que aparezca en una fuente y falte en otra es una alarma.
Cada entrada del inventario debe responder, como mínimo: quién la mantiene, quién la consume, qué versión está vigente, qué fecha de retirada tiene si está deprecada, y dónde está su openapi.yaml. Sin dueño no se despliega: es la única forma de que el inventario no envejezca.
- Gobierno con varios equipos
Año 3. Ya no hay un equipo, hay tres: catálogo, pedidos y suscripciones. Sin gobierno, en seis meses tienes tres APIs que parecen de tres empresas distintas: una con snake_case, otra con errores en texto plano, otra paginando con page/size.
El gobierno de Tienda Aroma se apoya en cuatro piezas, todas ya conocidas:
- La guía de estilo (02-01) como norma escrita, no como recomendación. Es la referencia que zanja discusiones en una revisión.
- La revisión de diseño: antes de escribir código, el equipo presenta el fragmento de
openapi.yamlpropuesto. Diez minutos con dos personas de otros equipos evitan meses de deuda. Se revisa el contrato, no la implementación. - Puertas automáticas en CI (05-04/05-05): Spectral valida el estilo y
oasdiffdetecta cambios rompedores. Lo automático no se discute, y eso es precisamente su virtud. - ADR en
docs/decisiones/: cada decisión estructural con su contexto, alternativas y consecuencias. Sirve para que dentro de dos años nadie "arregle" algo que estaba así a propósito.
# .github/workflows/ci.yml (extracto) — puertas de contrato
contrato:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Estilo del contrato (guia de estilo como codigo)
run: npx @stoplight/spectral-cli lint openapi.yaml --fail-severity=warn
- name: Cambios rompedores frente a la version publicada
run: |
git show origin/master:openapi.yaml > /tmp/openapi-publicado.yaml
npx oasdiff breaking /tmp/openapi-publicado.yaml openapi.yaml --fail-on ERR
# Si el cambio ES rompedor a proposito, se aprueba con la etiqueta
# "cambio-rompedor-aprobado" en el PR y un ADR enlazado. Nunca en silencio.Y el flujo humano completo de un cambio de contrato:
sequenceDiagram
participant E as Equipo
participant R as Revision de diseno
participant CI as CI (Spectral + oasdiff)
participant C as Consumidores
E->>R: Propuesta de cambio en openapi.yaml
R-->>E: Guia de estilo, ADR si es estructural
E->>CI: Pull request
CI-->>E: Rompedor detectado
alt No rompedor
CI->>C: Despliegue canary + changelog
else Rompedor
E->>R: ADR con alternativas
R-->>E: A la deuda de contrato o a v2
end
C-->>E: Metricas de uso y soporte
Errores Comunes y Consejos
- Depurar en caliente en lugar de mitigar. Cada minuto de diagnóstico durante un S1 se paga con presupuesto de error. Revierte, apaga la flag, y luego investiga.
- Post-mortems que terminan en "falta de atención". Si la acción no se puede cerrar con un enlace verificable, no es una acción. Y si el documento nombra a una persona como causa, la próxima vez nadie contará lo que pasó.
- Confundir "sin culpables" con "sin consecuencias". Las acciones tienen responsable y plazo, y se revisan en la retrospectiva del mes siguiente.
- Creer que añadir un campo nunca rompe. Sólo es cierto con clientes tolerant readers. Publica ese requisito en la guía de integración desde el día uno.
- Endurecer validaciones sin fase de aviso. Es el cambio rompedor que más se cuela porque no toca el esquema visible. Mide primero, rechaza después.
- Etiquetar métricas por ruta concreta o por
id. La cardinalidad estalla y te quedas sin observabilidad justo cuando la necesitas. Plantillas de ruta y conjuntos cerrados. - Apagar
v1por calendario sin mirar métricas por versión. Siempre aparece un consumidor olvidado. Las métricas mandan sobre el calendario; elSunsetanunciado no se adelanta, pero sí se puede retrasar. - Lanzar
v2por incomodidad interna. Si el dolor lo sufre sólo tu equipo, resuélvelo por dentro. Una versión nueva es una factura que pagan todos tus consumidores. - Tratar los 4xx como culpa del cliente. Son tu lista de tareas de documentación ordenada por impacto.
- Consejo final: publica el calendario de deprecación en un sitio estable y respétalo aunque duela. Un
Sunsetque se adelanta destruye más confianza que un incidente de 34 minutos.
Ejercicios
Ejercicio 1: clasificar cambios
Para cada cambio propuesto sobre v1 de Tienda Aroma, indica si es retrocompatible, si es rompedor, y cuál sería la estrategia correcta:
- Añadir el campo
origenCertificado(booleano) a la representación deCafe. - Añadir el valor
filtroal enumeradotuesteen las respuestas. - Cambiar el
limitepor defecto deGET /v1/cafesde 20 a 50. - Empezar a rechazar
precioMinnegativo con422 datos_invalidos. - Añadir el filtro opcional
?certificado=true.
Ejercicio 2: decidir sobre la v2
Una API interna de facturación acumula estas deudas: (a) el campo total está en euros como número decimal y provoca errores de redondeo; (b) GET /facturas no pagina y devuelve hasta 4.000 elementos; (c) el nombre factura_id usa snake_case mientras el resto de la API usa camelCase. Sólo hay dos consumidores, ambos internos. ¿Lanzarías una v2? Justifica con criterios, no con gustos.
Ejercicio 3: acciones de un post-mortem
Un despliegue del viernes por la tarde introdujo un fallo en la renovación de tokens: durante 18 minutos, todas las peticiones de Aroma Móvil recibieron 401 no_autenticado. Se detectó porque un usuario escribió en redes sociales; nadie del equipo vio ninguna alerta. Escribe cinco acciones para el post-mortem, cada una con tipo (correctiva, detección, preventiva, organizativa, comunicación) y un criterio de cierre verificable.
Soluciones
Ejercicio 1
| # | Cambio | Veredicto | Estrategia |
|---|---|---|---|
| 1 | origenCertificado |
Retrocompatible* | Añadir opcional y documentar. El asterisco: si algún consumidor valida con additionalProperties: false, sí rompe. Comprobar antes en la guía de integración y avisar en el changelog |
| 2 | Valor filtro en respuestas |
Rompedor en la práctica | Cualquier cliente con un switch exhaustivo sobre tueste fallará. Anunciar con 30 días, documentar el valor nuevo, y primero aceptarlo en peticiones (seguro) antes de emitirlo en respuestas |
| 3 | limite por defecto 20 → 50 |
Rompedor | Cambia el tamaño de página que el cliente recibe sin pedirlo; puede desbordar interfaces y multiplicar la carga. No cambiarlo en v1. Alternativa: documentar mejor limite y dejar el valor por defecto en v2 |
| 4 | Rechazar precioMin negativo |
Rompedor | Aunque sea "más correcto", peticiones antes aceptadas empiezan a fallar. Fase de aviso midiendo con superRefine durante 6 semanas; si el contador llega a cero, rechazar. Y 400 parametro_invalido, que es un parámetro de consulta, no un cuerpo |
| 5 | Filtro ?certificado=true |
Retrocompatible | Opcional; ausente = comportamiento actual. Añadirlo a openapi.yaml, con su índice en la base de datos (lección del INC-041) |
Ejercicio 2
No, todavía no. Análisis por deuda:
- (a)
totaldecimal en euros. Es real y grave (redondeo en dinero), pero se resuelve añadiendo: publicartotalCentimoscomo campo nuevo, documentarlo como preferente, deprecartotalen la documentación y medir su uso. No requiere versión. - (b)
GET /facturassin paginación. Se puede paginar de forma retrocompatible: aceptarlimite/desplazamiento, y mientras no se envíen, devolver el comportamiento actual. Con dos consumidores internos, se puede negociar además un límite máximo con aviso previo. No requiere versión. - (c)
factura_idensnake_case. Es incomodidad estética. Solución: emitir tambiénfacturaId(ambos campos conviven), documentar el nuevo, y eliminar el viejo cuando las métricas de uso lo permitan o llegue unav2motivada por otra cosa.
Además, con dos consumidores internos el coste de coordinación es bajísimo comparado con el de mantener dos versiones. Criterio aplicable: ninguna de las tres deudas obliga a romper, luego no hay v2. Lo que sí procede es abrir tres entradas en el registro de deuda de contrato con candidata_v2: true y revisarlas cada trimestre.
Ejercicio 3
| # | Acción | Tipo | Criterio de cierre |
|---|---|---|---|
| 1 | Corregir el fallo de renovación de tokens y desplegar con canary al 5 % | Correctiva | Commit enlazado + 24 h de canary con tasa de 401 en línea base |
| 2 | Alerta sobre tasa de 401 por cliente, disparo a los 3 min por encima del doble de la base |
Detección | Alerta creada y probada con una inyección controlada en preproducción |
| 3 | Prueba de integración del ciclo completo de renovación (token caducado → refresco → petición) en CI | Preventiva | Prueba en node:test + Supertest ejecutándose en ci.yml y fallando si se revierte el arreglo |
| 4 | Política de congelación de despliegues los viernes a partir de las 15:00 salvo correcciones urgentes | Organizativa | Regla publicada en el repositorio y comprobación automática en el pipeline |
| 5 | Nota en la página de estado y aviso a los usuarios afectados de Aroma Móvil con resumen del incidente | Comunicación | Entrada publicada con enlace al post-mortem |
Observa que la acción 2 es la más valiosa: el fallo duró 18 minutos, pero el problema real es que la detección llegó desde fuera. Un incidente que descubre un usuario en redes sociales es, ante todo, un fallo de observabilidad.
Conclusión
Diseñar una API es un ejercicio acotado; mantenerla es un compromiso indefinido. En esta lección hemos recorrido tres años de la API de Tienda Aroma y hemos visto que casi todo el trabajo posterior al lanzamiento consiste en proteger a quien ya confía en ti: vigilar las señales adecuadas desde el primer día, mitigar antes de diagnosticar cuando algo se rompe, escribir post-mortems que arreglen el sistema en vez de buscar responsables, escuchar lo que los 4xx y la analítica de uso te están diciendo, anotar la deuda de contrato en lugar de fingir que no existe, agotar todo lo que se puede añadir sin romper, y —sólo cuando ya no queda alternativa— planificar una v2 con su calendario, sus cabeceras Deprecation y Sunset, sus brownouts y su 410 final. A eso se suma el mantenimiento que nadie aplaude: CVE, versiones de Node, rotación de claves, revisión de SLO, inventario sin zombis y un gobierno con Spectral, oasdiff y ADR que mantenga la coherencia cuando ya no hay un solo equipo.
La conclusión de fondo es sencilla y exigente a la vez: una API es un compromiso a largo plazo con quien la consume. Cada campo publicado es una promesa, cada Sunset anunciado es un contrato, y la confianza que construyes durante tres años puede perderse con un cambio "inocuo" desplegado un viernes por la tarde. La calidad de una API no se mide el día del lanzamiento, sino en la facilidad con la que un integrador sigue trabajando con ella cuando ya nadie del equipo original queda en la empresa.
Con esto cerramos el recorrido de casos prácticos y análisis. Te toca a ti: en 06-04, Proyecto final: Diseñar y desarrollar tu propia API RESTful, aplicarás por fases todo lo aprendido —diseño del contrato, implementación, seguridad, documentación y despliegue— sobre un dominio propio, con una rúbrica clara para autoevaluarte. Todo lo que hemos hecho hasta ahora estaba preparando ese encargo.
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
