Inicio / Artículos / Detectando la deriva silenciosa en los contratos de API con tipos inferidos a partir de ejemplos y Zod

Detectando la deriva silenciosa en los contratos de API con tipos inferidos a partir de ejemplos y Zod

Por qué los tipos de TypeScript escritos a mano para APIs de terceros se vuelven obsoletos, cómo la inferencia de tipos y esquemas Zod a partir de las respuestas reales ayuda, y cómo las diferencias de instantáneas revelan desviaciones.

1565 palabras

Las APIs de terceros cambian sin previo aviso, y TypeScript no se dará cuenta, porque sus tipos describen cómo era la respuesta cuando alguien los escribió, no cómo es hoy en día. Este artículo explica cómo ocurre ese fallo, por qué generar tipos a partir de varias respuestas reales es mejor que escribirlos a mano, y por qué lo que realmente te protege es comparar las nuevas respuestas con una instantánea guardada. También verás dónde encaja este enfoque junto con OpenAPI y las pruebas de contrato, y dónde no lo hace.

Cómo un campo con nombre cambiado escapa sin ser detectado

Considere una interfaz de usuario que se integra con un proveedor de pagos. Un día, un campo en una de las respuestas del proveedor cambia de user_id a userId. No hay entrada en el registro de cambios, ni anuncio ni actualización de versión. Lo más probable es que un ingeniero del lado del proveedor haya corregido un nombre inconsistente, su conjunto de pruebas haya dado positivo y el cambio se haya implementado.

Nada falla en el lado que consume los datos, y ese es precisamente el problema. El código sigue leyendo response.user_id, y TypeScript lo acepta, porque la interfaz fue definida a mano hace meses a partir de un ejemplo de Postman que ya no refleja la realidad. Esa interfaz sigue prometiendo un campo user_id. En tiempo de ejecución, el valor es simplemente undefined. Durante dos semanas, tres rutas de código escriben silenciosamente undefined en un campo de cantidad, hasta que finalmente un ticket de soporte revela el error. No se dispara ninguna alerta, ninguna compilación falla, y la aplicación sigue haciendo lo incorrecto sin quejarse.

Los equipos que trabajan con APIs externas durante suficiente tiempo casi siempre se encuentran con alguna versión de este tipo de incidente.

El punto débil está en la fuente de los tipos

TypeScript no es el culpable aquí. El problema radica en el origen de los tipos. Las interfaces suelen tratarse como si provinieran de algo autoritativo, como un esquema, un contrato o una única fuente de verdad. En la práctica, muchas de ellas provienen de una respuesta de ejemplo que alguien transcribió a mano. Esa interfaz se copia luego en varios otros archivos y se considera como un hecho, sin que nadie la vuelva a revisar hasta que algo falla.

El verdadero contrato es lo que la API devuelve en producción en este momento. Se encuentra en un servidor que no controlas y puede cambiar sin tu consentimiento. Los tipos escritos a mano son una instantánea de un momento ya pasado, y el compilador no tiene forma de saberlo.

También existe una brecha más profunda. Los tipos de TypeScript desaparecen en tiempo de compilación, por lo que nunca se verifica nada en tiempo de ejecución. Si la estructura del payload cambia, solo la validación en tiempo de ejecución en los límites, por ejemplo al analizar la respuesta con un esquema Zod, convierte un undefined silencioso en un error inmediato y visible.

Infere tipos a partir de varias respuestas reales

Lo que más ayuda aquí no es un TypeScript más avanzado ni generics más ingeniosos, sino un proceso mecánico: tomar las respuestas que realmente devolvió la API, generar tipos a partir de ellas y recibir una alerta tan pronto como la realidad deje de coincidir.

La entrada debe ser respuestas reales en JSON, no documentación ni un esquema. A partir de ellas, una herramienta puede inferir tanto un tipo de TypeScript como un esquema Zod equivalente. Utilizar varios ejemplos es más importante de lo que parece. Una respuesta muestra cómo puede verse un payload. Tres o cuatro respuestas revelan qué campos son realmente opcionales, cuáles a veces tienen el valor null, y qué elementos de array tienen formas inconsistentes. Un solo ejemplo siempre engaña por omisión.

El ejemplo a continuación incluye dos respuestas para el mismo recurso, una con user_id y otra con userId, y muestra el tipo de TypeScript y el esquema Zod inferidos de ambas:

// paste these two responses in...
[
  {
    "user_id": "pot_00009exampleP0tOxWb",
    "name": "Wedding Fund",
    "balance": 550100,
    "currency": "GBP",
    "created": "2025-11-09T12:30:53.695Z",
    "updated": "2025-02-26T07:12:04.925Z"
  },
  {
    "userId": "pot_00009exampleP0tOxWb",
    "name": "Wedding Fund",
    "balance": 550,
    "currency": "EUR",
    "created": "2025-11-09T12:30:53.695Z",
    "updated": "2025-03-26T07:12:04.925Z"
  }
]

// ...get this out typescript
type Root = {
  user_id?: string
  name: string
  balance: number
  currency: string
  created: string
  updated: string
  userId?: string
}[]

// or ... get this out zod
import { z } from 'zod'

const Root = z.array(z.object({
  user_id: z.string().optional(),
  name: z.string(),
  balance: z.number(),
  currency: z.string(),
  created: z.string(),
  updated: z.string(),
  userId: z.string().optional(),
}))

Fíjese bien en lo que indica el resultado de la fusión. Dado que cada nombre aparece solo en una muestra, tanto user_id como userId pasan a ser opcionales. Técnicamente eso es correcto, pero también oculta el cambio de nombre: el código que lee cualquiera de estos campos sigue realizando verificaciones de tipo, y una respuesta que no contenga ninguno de ellos también pasaría la validación del esquema Zod. Las muestras también sugieren un problema que la inferencia de tipos nunca podrá detectar: balance disminuye de 550100 a 550 mientras currency cambia, lo que podría indicar un cambio entre unidades monetarias menores y mayores. En ambos casos el tipo inferido es number. La inferencia le indica la estructura, pero no su significado.

Muchos generadores de código se detienen en este punto. Pasar de datos sin tipo a datos con tipo es útil, pero no resuelve el problema del desvío en los tipos.

Las capturas y las diferencias detectan el cambio

El paso más importante ocurre después de la generación. Una vez que los tipos se derivan de una respuesta real, dicha respuesta puede almacenarse como un instantáneo. Cada vez que obtengas una nueva muestra del mismo endpoint, la compararás con el instantáneo y obtendrás un informe preciso de los cambios, ya sea un campo con un nombre diferente, un valor que ahora puede ser nulo donde antes era simplemente una cadena, o una clave adicional que aparece dentro de un objeto anidado. En lugar de una descripción vaga como “algo falló en algún lugar”, podrás ver la diferencia exacta en su estructura.

Esa comparación es lo que diferencia a un generador de tipos de un detector de desviaciones. La generación de código permite pasar de la nada a tener código tipado. La detección de desviaciones evita que un cambio de nombre de user_id a userId quede sin ser detectado en producción durante semanas. En el ejemplo anterior, una diferencia de instantáneas indicaría “user_id eliminado, userId añadido” en lugar de permitir que ambos campos se conviertan silenciosamente en opcionales.

Mantenga las cargas de producción en su máquina

Para detectar un desvío real se necesitan datos reales; las cargas de trabajo sintéticas no revelarán los cambios que realmente importan. Por eso, la privacidad se convierte en un requisito de diseño. Las respuestas de producción pueden contener información del cliente, por lo que pegarlas en un formulario web que las suba a un servidor de terceros genera un nuevo riesgo en el manejo de datos. Las herramientas para esta tarea deben ejecutarse localmente, por ejemplo, íntegramente en la pestaña del navegador o como un script en su propio repositorio, de modo que las cargas de trabajo nunca salgan de su entorno.

Dónde encaja este enfoque y dónde no

La inferencia basada en muestras con detección de desvíos no sustituye a OpenAPI ni a una configuración de pruebas de contratos como Pact. Si usted es dueño tanto del proveedor como del consumidor y puede imponer un esquema desde la fuente, hágalo; esa es la mejor solución a largo plazo.

Esta técnica se dirige a la situación más común y menos glamorosa: consumes una API que no controlas, la documentación está desactualizada o falta, y generar un cliente a partir de una especificación OpenAPI no es una opción porque no existe tal especificación o nadie confía en ella. Eso describe la mayoría de las integraciones con procesadores de pagos, servicios internos propiedad de otros equipos y APIs de proveedores externos. En ese contexto, la respuesta real es la única verdad objetiva disponible, por lo que de ella deben derivarse tus tipos.

Mantenga el alcance restringido. Usar JSON, excluir TypeScript y Zod, además de la detección de desviaciones, cubre las necesidades. Intentar manejar XML, protobuf y cada caso límite de los esquemas convierte una herramienta eficaz en una vaga. Para conocer más sobre las decisiones relacionadas con los contratos que suelen afectar a las interfaces frontales, consulte errores comunes en contratos de API que dañan la confiabilidad del frontend.

Primer test práctico

El mejor lugar para probar esto es en una integración que ya haya sufrido cambios silenciosos en su estructura. Tome una respuesta antigua y otra reciente del mismo endpoint, páselas por el proceso de inferencia y realice una comparación mediante captura de estado, y revise qué elementos se marcan como problemáticos. Ver un cambio histórico real reflejado en la diferencia es más convincente que cualquier argumento a favor de este enfoque.

Puntos clave

  • Las interfaces manuscritas para APIs de terceros son instantáneas del pasado, y TypeScript no puede detectar cuándo se vuelven obsoletas.
  • Infiera tipos y esquemas Zod a partir de varias respuestas reales, ya que múltiples ejemplos revelan campos opcionales, nulos e inconsistentes que un solo ejemplo oculta.
  • Valide las respuestas en tiempo de ejecución en el límite de la API para que los cambios en su estructura se detecten claramente en lugar de generar undefined.
  • Almacene las respuestas como instantáneas y compare nuevas muestras con ellas; la inferencia por sí sola puede ocultar un cambio de nombre al presentarlo como dos campos opcionales.
  • Mantenga los datos de producción en el entorno local, y prefiera pruebas OpenAPI o basadas en contratos siempre que controle ambos lados de la API.

Lecturas relacionadas

  • JSON válido, contrato roto: verificaciones en capas para regresiones en el cargamento — Aprenda a detectar regresiones en el cargamento JSON que se parsean correctamente: diferencias semánticas, un JSON Schema enfocado, afirmaciones de reglas de negocio en Node y los límites de los tipos generados.
  • El método QUERY de HTTP para equipos frontend: lecturas seguras con cuerpo — Aprenda cuándo el método QUERY de HTTP es mejor que GET y POST para filtros complejos, cómo llamarlo con fetch y qué requisitos impone CORS, el caché y el soporte de infraestructura.