Prompts para Contract Testing y Validación de Schemas con IA
8 prompts para evitar que tu API rompa a otros equipos: contract testing, validación con OpenAPI, JSON Schema, Pact y tests de compatibilidad.
Tabla de contenidos
La historia es siempre la misma. Tu API funcionaba. Otro equipo la consume. Entras en su PR, cambias el campo total de número a string porque “así queda mejor”, y su build se rompe.
No es culpa suya. Tú no rompiste nada: nunca te dijo que ese campo era intocable. Ese es el problema, y tiene nombre: implicit contract.
Un contrato implícito es lo que tu equipo asume que hace la API sin haberlo escrito en ningún sitio donde una máquina pueda verificarlo. Es el más común y el más caro. Se rompe un viernes, desde otro repositorio, y el mensaje que aparece es un TypeError: expected number, got string en una pantalla que nadie de tu equipo ha visto jamás.
Este artículo te da los 8 prompts para convertir tu API en un contrato explícito, validado en CI y protegido contra breaking changes, y para que la IA te ayude a hacerlo con el código que ya tienes.
Si ya tienes la API funcionando y lo que quieres es documentarla, empieza por aquí. Si lo que tienes es una app con llamadas a la IA que fallan, eso es otro tema: la guía de function calling cubre la parte de conectar modelos con APIs.
Cinco reglas antes de definir el contrato
1. El contrato se escribe una vez y se genera el resto
Si tienes un .yaml de OpenAPI y además tipos de TypeScript escritos a mano, tienes dos fuentes de verdad que divergen. Elige una fuente primary: casi siempre el código, con generación del resto (tipos, clientes, validación, documentación). Escribirlo todo a mano triple la posibilidad de que uno se quede viejo.
2. Añadir es seguro, quitar no
Regla universal de diseño de APIs: cambios aditivos siempre son compatibles. Añadir un campo opcional, un endpoint, un valor de enum, un header de respuesta. Romper es quitar un campo, cambiar su tipo, hacerlo obligatorio, alterar un valor de enum existente, cambiar un status code o mover un endpoint.
3. Valida en la frontera, no en el interior
Una API que no valida su entrada está violando su propio contrato. La validación con Zod o ajv tiene que estar en el borde: ahí es donde puedes devolver un 400 claro en lugar de un 500 con un stack trace en producción.
4. Los errores también son contrato
El 400 con {"error": {"code": "INVALID_TOTAL", "message": "..."}} es un contrato tan firme como el 200. El 500 Internal Server Error vacío es una decisión que tu consumidor va a acabar pagando. Códigos de error estables desde el día uno.
5. El consumidor manda en el ritmo, el proveedor en el detalle
No puedes obligar a que te actualicen a una v2 mañana. Por eso el versionado y los campos deprecados con fecha existen. Un contrato vivo tiene deprecated: true y x-sunset-date, no una reunión de aviso.
1. Extraer el contrato implícito desde el código existente
Este es el punto de partida cuando tienes una API que ya funciona y nunca documentó.
Prompt:
Tengo esta API en producción y quiero extraer su contrato real:
ARCHIVOS: [PEGAR RUTAS: controllers, schemas, tipos]
STACK: [Node.js + Express / Fastify / NestJS / Next.js route handlers]
BASE DE DATOS: [POSTGRES / MONGO / SQL]
Genera un contrato OpenAPI 3.1 en YAML que refleje lo que el código HACE
REALMENTE ahora, no lo que dice la documentación (si existe, ignórala).
Para cada endpoint extrae:
- método, path, query params, path params, headers
- request body: schema JSON real, con required, tipos, formatos, defaults,
límites (min/max/minLength) que seapliquen de verdad en el código
- response 200: schema real, no el ideal
- CÓDIGO de error: TODOS los status que puede devolver ese endpoint, con
su body real. Busca los throw, los next(err), los status() del código y
no te quedes en el happy path.
- autenticación requerida por endpoint (lee los middlewares)
- headers de respuesta que importan (RateLimit, Location, ETag)
- rate limiting si lo hay
Añade un campo description por cada propiedad que usarías para documentarlo,
y marca en example un body real quehas construido.
Al final, lista los endpoints donde el código y la documentación actual
discrepan, si es que hay documentación.
Devuelve el YAML completo. No inventes endpoints que no estés leyendo.
Por qué funciona: pedir explícitamente el happy path más los errores suele revelar que la mitad de los endpoints nunca documentó sus estados de fallo. Y extractlo del código garantiza que el spec inicial describe la realidad, no la intención.
2. Validar request y response con Zod: del schema al runtime
Un schema que solo sirve para generar tipos no valida nada. La gracia de Zod es tener una sola definición que hace las dos cosas.
Prompt:
Tengo este schema Zod:
[PEGAR SCHEMA ZOD]
Y este endpoint que lo consume:
[PEGAR HANDLER]
Genera la capa de validación completa:
1. parse del body, query y params, con errores en formato de mi API:
{ "error": { "code": "VALIDATION_ERROR", "message": "...",
"details": [{ "path": "items[0].total", "message": "..." }] } }
2. Un middleware reusable que valida y devuelve 400 con ese formato.
3. Validación del RESPONSE también: en desarrollo, un wrapper que valida
lo que sale y loguea un error si no cumple el schema. Esto detecta
las regresiones de contrato en tu propio CI antes de desplegar.
4. Tipos inferidos: z.infer, con el input y el output diferenciados
(los .default() cambian el tipo de salida respecto al de entrada).
5. Un test con vitest que cubra: body válido, cada campo requerido ausente,
cada tipo incorrecto, y los casos borde (array vacío, string de longitud
máxima, null vs undefined).
Escribe TypeScript con tipos estrictos, sin `any`.
Por qué funciona: el paso 3 es el que casi nadie hace y el que más valor da. Validar la respuesta en desarrollo convierte un fallo de contrato en desarrollo en un error legible en tu CI.
3. Validar contra OpenAPI con ajv en tiempo de ejecución
Si tienes un contrato publicado y terceros lo consumen, la validación tiene que ser contra el contrato, no contra tus tipos internos.
Prompt:
Tengo un contrato OpenAPI 3.1 en openapi.yaml y quiero validar el tráfico
real contra él en tiempo de ejecución:
STACK: [Node.js + Express, o Fastify]
ENTORNO: [producción con alto tráfico]
Genera:
1. Configuración de ajv con el draft 2020-12, strict: false (los schemas de
OpenAPI tienen keywords que ajv no conoce) y los formatos de OpenAPI
sobreescritos para JSON Schema (int32, int64, date-time, uuid, uri).
2. Un middleware que:
- valide request (body, params, query, headers) y devuelva 400 con el
path exacto del campo que falla
- valide response en modo "shadow": si falla, solo registra el error con
el path del endpoint y no rompe la petición. Esto es crítico: si
bloqueas responses inválidos en producción, un fallo de contrato se
convierte en una caída.
- en staging, sí devuelve 500 si el response no cumple.
3. Normalización previa: ajv no entiende nullable en OpenAPI 3.0 ni
nullable vs optional en 3.1. Crea un pre-proceso que convierta el schema.
4. Un script que descargue el spec de un entorno anterior (por ejemplo el
deployed hace una hora) y lo compare con el actual para detectar cambios
antes de desplegar.
5. Métricas: cuéntame cuántos requests fallan validación por endpoint y por
campo, y expónlas en formato Prometheus.
Devuelve los ficheros TypeScript completos.
Por qué funciona: el modo shadow es el detalle que hace viable esto en producción. Aprendes de los fallos sin crear ninguno. Y el 3 es un problema real: el mismo semántica (nullable) se representa distinto en OpenAPI 3.0 y 3.1.
4. Detectar breaking changes en el CI
Prompt:
Quiero un job de CI que detecte breaking changes en mi API antes de
mergear:
ARCHIVO DE SPEC: [openapi.yaml]
SPEC PUBLICADO: [PEGAR URL, ej. https://api.ejemplo.com/openapi.json]
CI: [GITHUB ACTIONS]
Genera:
1. oasdiff breaking openapi.yaml <spec-publicado> con la configuración que
ignore las adiciones opcionales pero marque como breaking:
- eliminación de campo, endpoint o parámetro
- cambio de tipo (string → number, object → string)
- campo opcional que pasa a obligatorio
- nuevo valor en un enum con restriction (si el enum es cerrado)
- cambio de un status code de éxito
- reducción de min/max, minItems/maxItems, minLength
- eliminación de un security scheme o reducción de permisos
2. El fichero workflow que descarga el spec publicado, ejecuta oasdiff y
falla el job si hay breaking changes.
3. Un script "spec:accept" para cuando el cambio es intencionado: actualiza
el snapshot en versionado, genera un CHANGELOG.md con los cambios
clasificados (BREAKING / added / deprecated / fixed) y pide confirmación.
4. Un endpoint /version o el campo api-version en el health check que
devuelva el hash del spec desplegado, para poder comparar contra lo que
realmente está en producción y no contra un spec stale.
Incluye cómo generar el CHANGELOG con breaking changes en negrita, que es
lo que leen los otros equipos.
Por qué funciona: comparar contra el spec publicado y no contra git es el detalle clave. Si comparas contra el spec del main, otro equipo ya ha consumido la versión nueva y tu check pasa tarde.
Si quieres velocidad a la hora de detectar el fallo, la calculadora de precios de IA no aplica aquí, pero sí aplica si tu API es un proxy de modelos: ahí cada llamada es un coste y los errores 429 son un riesgo constante que sí conviene diseñar con margen.
5. Montar Pact para contratos consumer-driven
Con OpenAPI verificas que cumples lo que declaras. Con Pact verificas que sigues cubriendo lo que tus consumidores usan.
Prompt:
Quiero implementar contract testing consumer-driven con Pact:
PROVEEDOR: API de [NOMBRE] en [REPO], stack [Express/Fastify]
CONSUMIDOR 1: [SEGUNDO EQUIPO/REPO], usa [GET /pedidos, POST /pedidos]
CONSUMIDOR 2: [OTRO CONSUMIDOR], usa [GET /pedidos/:id]
Genera:
1. En el repo del CONSUMIDOR: los tests de contrato con @pact/pact-jest o
@pact/pact-js (los tests se ejecutan contra un mock del proveedor, sin
necesitar el proveedor levantado).
- Cada interaction con: given, when, then (state, request, response).
- Usa providerStates para los datos que el mock necesita.
- Genera el JSON del contrato en contracts/ y súbelo al broker.
2. En el repo del PROVEEDOR: el test de verificación que descarga los
pactos del broker y levanta la API real para comprobar que cumple.
- providerStates resueltos con datos de seed reales.
- Mocks deshabilitados (allowMultiple: false) para que si el proveedor
llama a una dependencia que no toca el contrato, salte.
3. docker-compose con el Pact Broker para desarrollo local.
4. El workflow de GitHub Actions para ambos repos, con el broker en CI
(usando el docker image oficial, sin SaaS).
5. Explica qué NO cubre Pact y cómo se complementa con los tests de
integración normales.
Un detalle: el JSON del contrato es un artefacto versionado. Debe estar en
el repo del consumidor, commiteado, y los cambios en él deben generar un
CHANGELOG para el proveedor.
Devuelve los ficheros de test completos y ejecutables.
Por qué funciona: los providerStates son la parte que más se olvida y la que hace que el test del proveedor falle por datos en lugar de por el contrato. Sin provider states bien resueltos, el falso positivo es constante.
6. Detectar incompatibilidades desde el lado del consumidor
El otro equipo no tiene por qué esperar a que publiques una breaking change.
Prompt:
Soy el consumidor de esta API. Este es el cliente con el que llamo:
[PEGAR CLIENTE: axios/fetch generado, o el código de las llamadas]
Y esta es una versión nueva del OpenAPI que el proveedor quiere desplegar:
[PEGAR openapi.yaml NUEVO]
La que uso hoy es: [PEGAR openapi.yaml VIEJO o URL]
Analiza el impacto en MI código:
1. ¿Qué llamadas mías romperían con este spec? Lista endpoint + método +
línea de mi cliente.
2. ¿Qué campos que yo leo ya no existen o han cambiado de tipo?
3. ¿Qué campos nuevos obligatorios tendría que enviar?
4. ¿Qué valores de enum uso que ya no son válidos?
5. ¿Cuánto trabajo de migración implica? Estima: solo ajustes de tipos,
o hay que cambiar lógica.
Y dame el código de migración: el cliente actualizado con los tipos
correctos, un flag de versión si coexisten ambas versiones, y los tests que
verifican que mi código sigue funcionando contra la versión antigua
mientras migro.
Devuelve un informe de impacto + el código de migración.
Por qué funciona: poner al consumidor a hacer el análisis antes del merge del proveedor convierte una sorpresa en un trabajo planificado. Y el cliente con flag de versión es lo que permite migrar sin ventana de mantenimiento.
7. Diseñar el versionado y una estrategia de deprecación
Prompt:
Necesito una estrategia de versionado y deprecación para mi API:
USUARIOS INTERNOS: [n equipos, se despliegan a diario]
USUARIOS EXTERNOS: [n clientes, no controlo su despliegue]
CAMBIO PENDIENTE: [DESCRIBIR: eliminar campo X, cambiar tipo de Y]
Quiero un enfoque REST, no GraphQL.
Genera:
1. Comparación de estrategias: versionado en path (/v1/, /v2/) vs header
(Accept: application/vnd.api+2) vs fechas en el path. Con pros y contras
reales para mi caso, no genéricos. Empieza por recomendar una.
2. Plan de deprecación completo:
- Cómo se marca la deprecación en el spec (deprecated: true en OpenAPI,
Sunset header RFC 8594).
- El flujo real: aviso → periodo de convivencia → apagado.
- Qué headers se devuelven durante la deprecación y qué dice cada uno
(Deprecation, Sunset, Link rel="deprecation").
- Cómo medir el uso de cada versión para saber cuándo se puede apagar
v1 (métrica concreta en el código).
3. Código del middleware que inyecta los headers de deprecación, y del
endpoint /status que reporta el uso por versión.
4. Qué pasa con los webhooks que TÚ emites a otros equipos: ¿también se
versionan? ¿Cómo se comunica el cambio de payload?
Un punto clave: quiero poder apagar la v1 en base a datos, no en base a una
fecha. Dame la métrica exacta.
Por qué funciona: el punto 5 es donde se enfocan casi todos los proyectos y donde más caro sale. Si tu API emite eventos o webhooks, ese canal es otro contrato, con otros consumidores, y suele estar mucho menos cuidado que el REST. La guía de webhooks cubre a fondo el caso de Stripe, que es donde más se nota.
8. Generar tests de contrato a partir del spec
Prompt:
Tengo este OpenAPI 3.1: [PEGAR openapi.yaml]
Stack de test: [VITEST + SUPERTEST / PLAYWRIGHT]
Objetivo: generar tests de contrato que cubran cada endpoint y cada
status code declarado.
Genera:
1. Un test por endpoint y por status code declarado en el spec:
- response válido completo
- request con todos los campos requeridos
- cada status de error: verifica status Y el schema del body de error
- campos requeridos ausentes en el request (por cada uno)
- tipos incorrectos
- unknown properties: ¿el endpoint las rechaza (additionalProperties: false)
o las ignora? Testea el comportamiento REAL.
2. Un test de property-based testing con fast-check: genera inputs aleatorios
válidos según el schema y verifica que nunca hay un 500. Un 500 con
input que cumple el schema significa que hay un fallo de validación en
el código.
3. Verificación de que el response cumple el schema DECLARADO, no solo
código válido.
4. Un informe que compare: endpoints declarados en el spec vs endpoints
realmente implementados vs endpoints testeados. Los tres conjuntos
deberían ser iguales; si no, esa diferencia es lo que hay que arreglar.
Escribe TypeScript. Genera un helper para validar contra el schema que
puedas reutilizar.
Por qué funciona: el property-based testing (punto 2) es lo que encuentra los huecos reales. Los tests escritos a mano prueban lo que el autor pensó; los generados a partir del schema prueban todo el espacio del schema.
El catálogo de herramientas y cuándo usar cada una
| Herramienta | Qué valida | Momento | CI bloquea | Limitación principal |
|---|---|---|---|---|
| Zod | Tu shape de datos en runtime | En cada request | Sí (con types) | No valida contra el spec publicado |
| ajv + OpenAPI | Contrato publicado, request y response | En cada request (shadow) | En staging | Overhead; hay que normalizar nullable |
| oasdiff | Breaking changes entre versiones | En PR | Sí | Solo compara specs, no runtime |
| Pact | Lo que tus consumidores usan de verdad | En PR del proveedor | Sí | Requiere broker y provider states |
| Schemathesis | Property-based sobre OpenAPI | En nightly | Opcional | Requiere la API levantada |
| fast-check | Generador de inputs desde schema | En cada PR | Sí | Hay que decidir qué oráculo usar |
| SonarQube | Deuda técnica y complejidad | Continuo | No | No entiende semántica de API |
| OpenAPI diff UI | Comparación visual de specs | En revisión | No | Manual, no automatizable |
La combinación que mejor funciona en la práctica es Zod en runtime + oasdiff en CI + Pact entre equipos con SLA acordado. Los tres cubren capas distintas: la forma de los datos, la compatibilidad con lo publicado y la compatibilidad con lo que te piden.
Anti-patrones en contratos
❌ El spec como documento decorativo
# ❌ openapi.yaml que nadie ejecuta, escrito a mano en 2023
components:
schemas:
Pedido:
properties:
total:
type: number # el código ya dice string desde hace 6 meses
// ✅ El spec se valida contra la realidad en cada request y en cada PR
const responseSchema = compile(schemaPedido);
if (!responseSchema(cuerpo)) {
logger.error({ path, errors: responseSchema.errors }, 'contrato roto');
}
❌ Introducir un campo opcional para todo
// ❌ "Por si acaso lo necesito luego"
interface Pedido {
id: string;
cliente?: Cliente;
/items?: Item[];
metadata?: Record<string, unknown>; // cajón de sastre
legacy_id?: string; // nadie sabe qué es
}
// ✅ Campos explícitos, deprecaciones declaradas con fecha
interface Pedido {
id: string;
cliente: Cliente;
items: Item[];
/** @deprecated desde 2026-03-01, se elimina 2027-01-01. Usar externalRef. */
legacy_id?: string;
}
❌ Validar solo el request
// ❌ Solo comprueba que lo que entra es válido
app.post('/pedidos', validateBody(pedidoSchema), crearPedido);
// ✅ Valida también lo que sale: detecta regresiones de contrato
app.post('/pedidos', validateBody(pedidoSchema), crearPedido);
app.use(validateResponse(pedidoSchema, { mode: process.env.NODE_ENV === 'production' ? 'shadow' : 'strict' }));
❌ Versionar por miedo a cada cambio
// ❌ Tres versiones para tres campos opcionales
router.use('/v1', v1); router.use('/v2', v2); router.use('/v3', v3);
// ✅ Una versión, campos aditivos, deprecación con fecha
router.use('/v1', v1);
// v2 solo cuando el cambio sea realmente incompatible,
// y con periodo de convivencia medido por uso real
❌ Breaking change en un PR “pequeño”
# ❌ "Solo quito este campo, no merece un changelog"
- total: number
+ total: string
Con oasdiff en el CI, esto ni llega a merge. Y no debería llegar. El coste de un de-merge es una tarde; el coste de romper el build de otro equipo es un incidente.
Errores frecuentes y cómo se resuelven
El spec se desincroniza del código en dos semanas
Causa casi siempre la misma: dos fuentes de verdad. Elige el código como fuente primary y genera el spec (con tsoa, zod-to-openapi, NestJS Swagger o los decorators de NestJS). Si necesitas editar el spec a mano, escribe un test que compare el spec generado con el commiteado y falle en CI si difieren.
Pact falla en el proveedor con “unexpected request” El proveedor está llamando a una dependencia que el contrato no cubre, o el providerState no está configurado. Los mocks de Pact son estrictos por diseño: por eso son útiles. Resuélvelo preparando el estado en el providerStates del test del proveedor, no desactivando el mock.
El breaking change lo detecta tu equipo tarde El problema es la dirección de la verificación. Si solo verificas tu spec contra el anterior, pero otro ya consumió la versión anterior, no hay nada que hacer. Lo que funciona: publicar el spec y que los consumidores lo verificen en su CI, o usar Pact, donde el consumidor declara qué necesita y tu CI lo comprueba.
Los errores 400 son un {"message": "Bad Request"} genérico
No es un detalle de UX, es un contrato roto. El consumidor no puede distinguir entre “campo faltante” y “valor inválido” si el mensaje es el mismo, así que acaba haciendo try/catch sobre el texto. Define un formato único de error desde el primer día y aplícalo con un helper, no endpoint por endpoint.
El rate limiting rompe los tests de integración Los tests de contrato hacen muchas peticiones seguidas y los limitadores los cortan. En el entorno de test, desactiva el limitador por IP o sube el umbral hasta 10x; pero prueba el limitador en un test aparte, con su propio presupuesto, para que no desaparezca de la cobertura.
Schemathesis genera un 500 con un input perfectamente válido
Ha encontrado un bug real. Casi siempre es una excepción no capturada en el camino feliz: una dependencia que devuelve null, una división por cero, un await que nadie esperó. Este es el mejor hallazgo que puede darte la IA: un caso de entrada que cumple tu propio contrato y que tu contrato no captura.
El flujo completo
┌───────────────────────────────────────────────────────────┐
│ CÓDIGO EXISTENTE (Fastify + Zod) │
└───────────────────────────┬───────────────────────────────┘
▼ prompt 1
┌───────────────────────────────────────────────────────────┐
│ openapi.yaml extraído del código real (incl. errores) │
└───────────┬───────────────────────────────────────────────┘
▼ prompt 2
┌───────────────────────────────────────────────────────────┐
│ Zod: validación de request y shadow de response │
└───────────┬───────────────────────────────────────────────┘
▼ prompt 3
┌───────────────────────────────────────────────────────────┐
│ ajv + OpenAPI en runtime (shadow en prod, strict en CI) │
└───────────┬───────────────────────────────────────────────┘
▼ prompt 8
┌───────────────────────────────────────────────────────────┐
│ Tests de contrato: cada status, cada campo, property-based│
└───────────┬───────────────────────────────────────────────┘
▼ prompt 4
┌───────────────────────────────────────────────────────────┐
│ CI: oasdiff contra el spec PUBLICADO → ¿breaking? │
└─────┬───────────────────────────────┬─────────────────────┘
│ NO breaking │ BREAKING
▼ ▼
┌───────────────┐ ┌─────────────────────────────┐
│ Merge │ │ spec:accept → CHANGELOG │
└───────────────┘ │ + deprecación con Sunset │
└──────────────┬──────────────┘
▼
┌──────────────────────┐
│ prompt 5: Pact │
│ consumidores │
│ declaran qué usan │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ prompt 6: análisis │
│ de impacto │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ Apagado de v1 por │
│ MÉTRICA, no fecha │
└──────────────────────┘
El bucle se cierra en dos sitios: el spec publicado (que se compara en cada PR) y el broker de Pact (que te dice qué piden tus consumidores). Ninguno de los dos requiere que nadie se acuerde.
Conclusión
Una API sin contrato explícito no es una API sin documentación: es una API con documentación implícita, que es peor, porque es invisible y no se puede verificar.
Estos 8 prompts cubren el ciclo entero: extraer el contrato desde el código que ya tienes, validarlo en runtime con Zod y ajv, generar los tests que cubren cada status declarado, detectar breaking changes en el CI antes de que se fusionen, y cerrar el círculo con Pact para saber qué necesitan tus consumidores de verdad.
Lo que cambia el juego no es la herramienta, es la dirección de la verificación. Mientras solo compruebes tu spec contra el anterior, alguien va a romperse por ti. Cuando cada consumidor verifica su propio uso contra el spec publicado, el problema se convierte en un mensaje de CI de quien lo va a sufrir.
Si tienes la API funcionando pero te da pereza escribir el spec porque no sabes cómo empezar, el prompt 1 te lo hace en una pasada. Y si el problema que tienes no es de contrato sino de que tu app se rompe cuando el modelo responde raro, vuelve a parsear JSON con IA: la validación en la frontera empieza por ahí.
Y si quieres estructurar alguno de estos prompts antes de pegarlos, el generador de prompts te ahorra el trabajo de montar el bloque con el contexto, el rol y el formato de salida.
Un contrato bien definido no te impide ser flexible. Te da permiso para cambiar rápido, porque sabes qué te va a romper y a quién.