Saltar a contenido

Evolucionar el contrato (OpenAPI + SSE)

Esta guía explica cómo ampliar el contrato API del backend (esquema OpenAPI y eventos SSE) sin romper a los clientes ya desplegados, siguiendo la política autorizada por el owner.

El "contrato" tiene dos piezas:

  • OpenAPI — el esquema REST congelado en backend/tests/contract/openapi_snapshot.json.
  • SSE — los eventos en streaming (chat y bus de sesión) inventariados en backend/tests/contract/sse_events_inventory.md.

El gate pytest tests/contract/ (parte de los gates duros de CI) bloquea cualquier drift no intencionado contra estos ficheros.


La regla de oro: aditivo y junto

Autorización permanente del owner (2026-06-13)

El contrato SÍ se puede ampliar de forma ADITIVA sin sign-off (añadir campos a respuestas, añadir paths/endpoints nuevos, ampliar el contrato SSE), con una única condición innegociable: en el mismo cambio se actualiza el frontend para consumir lo nuevo. Backend y frontend viajan juntos; nunca un campo de contrato nuevo sin su consumo en el front.

Qué entra en cada categoría:

Tipo de cambio ¿Sign-off? Por qué
Añadir un campo nuevo a una respuesta No (aditivo) Los clientes viejos lo ignoran
Añadir un endpoint/path nuevo No (aditivo) No afecta a rutas existentes
Añadir un evento SSE nuevo o un campo a un evento No (aditivo) Los consumidores viejos lo ignoran
Eliminar un campo / endpoint / evento Sí, con cuidado Rompe clientes desplegados
Renombrar un campo / endpoint / evento Sí, con cuidado Equivale a remove + add
Cambiar el tipo de un campo existente Sí, con cuidado Rompe la deserialización

Removals, renames y type-changes

Estos cambios rompen clientes ya desplegados y siguen necesitando cuidado y revisión. No los apruebes automáticamente.


Pasos para una adición

1. Editar el esquema (backend)

Modifica el esquema Pydantic o el router correspondiente para añadir el campo o endpoint nuevo. Sigue las convenciones del backend (ver Hexagonal y backend/AGENTS.md):

  • Schemas con model_config = ConfigDict(from_attributes=True).
  • HTTPException solo en la capa interface/.
  • Tipos modernos (X | None, colecciones parametrizadas).

2. Regenerar el snapshot OpenAPI

El test backend/tests/contract/test_openapi_snapshot.py compara el app.openapi() en vivo contra openapi_snapshot.json. Para regenerar el snapshot tras un cambio intencionado, ejecuta desde backend/:

UPDATE_OPENAPI_SNAPSHOT=1 pytest tests/contract/test_openapi_snapshot.py

Esto reescribe tests/contract/openapi_snapshot.json con el esquema normalizado (claves ordenadas, listas conocidas ordenadas; se descartan servers e info.version para que no generen ruido).

Revisa el diff antes de commitear

Tras regenerar, inspecciona el cambio para confirmar que solo son adiciones:

git diff tests/contract/openapi_snapshot.json

La aprobación del diff es para humanos — no la deleguen a aprobación automatizada.

3. Documentar el evento SSE (si aplica)

Si el cambio toca el streaming, actualiza el inventario backend/tests/contract/sse_events_inventory.md. Recuerda las convenciones de wire-format del propio inventario:

  • Formato: data: <json>\n\n (solo campo data:; no se usan event:/id:/retry:).
  • El tipo del evento viaja dentro del JSON como campo type.
  • Eventos por bus llevan la envoltura { "type", "data", "timestamp" }; los emitidos inline en el stream de chat van con type y data planos, sin timestamp.

Documenta el evento o campo nuevo en la sección del endpoint correspondiente.

4. Regenerar los tipos del frontend (codegen)

El frontend genera sus tipos TypeScript a partir del mismo snapshot. Desde frontend/:

npm run codegen

Este script ejecuta openapi-typescript sobre ../backend/tests/contract/openapi_snapshot.json (o $OPENAPI_SNAPSHOT si está definido) y escribe lib/api/generated/openapi.d.ts.

Después, cablea el consumo en el frontend en el mismo PR: usa el campo o endpoint nuevo en el código de la app. Esto cumple la condición innegociable de la regla de oro.


Verificación: el gate de contrato

El gate duro de CI que protege el contrato es:

pytest tests/contract/
  • Si hay drift entre app.openapi() y el snapshot, el test test_openapi_snapshot_matches falla con el DeepDiff y te recuerda el comando de regeneración.
  • Si el snapshot falta, el test falla pidiendo que lo generes.

Ejecuta este gate localmente antes de abrir el PR para asegurarte de que el snapshot está sincronizado con el esquema en vivo.


Checklist del PR

  • [ ] Esquema/router editado (solo adiciones, o sign-off para removals/renames/type-changes).
  • [ ] UPDATE_OPENAPI_SNAPSHOT=1 pytest tests/contract/test_openapi_snapshot.py ejecutado.
  • [ ] git diff tests/contract/openapi_snapshot.json revisado por un humano (solo adiciones).
  • [ ] sse_events_inventory.md actualizado si tocaste streaming.
  • [ ] npm run codegen ejecutado en el frontend.
  • [ ] Frontend cableado para consumir lo nuevo en el mismo PR.
  • [ ] pytest tests/contract/ en verde.
  • [ ] Referencia de endpoints regenerada: python scripts/gen_openapi_reference.py (o make docs-build, que la regenera desde el snapshot). Ver Referencia de API · Endpoints.