Cambios incompatibles de API, explicados

Un cambio incompatible de API es cualquier cambio en el contrato publicado de una API que puede hacer que un cliente escrito correctamente contra el contrato antiguo falle contra el nuevo, sin que el cliente cambie nada. Esa única frase es toda la teoría; todo lo demás en este artículo son sus consecuencias: qué cambios cuentan, qué trampas famosas parecen inofensivas y no lo son, cómo revisar una subida de especificación antes de que se despliegue y cómo las estrategias de versionado compran tiempo a los clientes. Para aplicarlo a dos documentos reales, ejecuta OpenAPI Diff sobre las especificaciones antigua y nueva y lee los veredictos junto a esta página.

Un contrato es una promesa sobre el futuro

Una especificación de API no es documentación en el sentido casual; es una promesa. Cuando una especificación declara que GET /pets acepta un parámetro opcional limit y responde 200 con un objeto que contiene una cadena name, cada autor de clientes construye exactamente sobre eso: el parámetro se omite cuando no hace falta, el estado se bifurca, el campo se lee como cadena. La corrección del cliente se juzga contra la promesa, y por eso la definición de incompatible es asimétrica: un cambio es incompatible si un cliente antiguo correcto puede fallar, aunque la mayoría de los clientes sobreviva por casualidad. Un SDK estricto generado desde la especificación antigua, fallando por un tipo cambiado, basta: se rompió la promesa, no el cliente.

Este marco resuelve la mayoría de las discusiones sobre si un cambio «cuenta». Eliminar un endpoint es incompatible porque la promesa incluía su existencia. Añadir un endpoint no lo es, porque ningún cliente antiguo se prometió nada sobre rutas que nunca llama. Un campo de respuesta opcional nuevo es seguro porque los analizadores antiguos ignoran los campos desconocidos: en cualquier formato sensato, la salida aditiva no puede invalidar a un lector. Una entrada obligatoria nueva, en cambio, alcanza cada petición existente e invalida las que la omiten. La prueba es siempre la misma: reproduce el tráfico válido de ayer contra las reglas de mañana y pregunta qué falla ahora.

Dos corolarios importan en la práctica. Primero, el contrato es lo que se publica, no lo que el servidor hace hoy por casualidad: un campo no documentado que los clientes descubrieron husmeando el tráfico no está prometido, y retirarlo es grosero pero no, estrictamente, incompatible — lo que es un argumento para documentar lo que pretendes mantener y nada más. Segundo, el contrato incluye toda la superficie de los estándares sobre los que se apoya: la semántica de métodos y estados HTTP fijada por RFC 9110 forma parte de la promesa, así que redefinir lo que significa un 200 o un 404 es un cambio de contrato aunque el archivo de especificación nunca mencione el significado de los estados.

Las trampas clásicas

La ruptura de producción más común es la incorporación de un campo obligatorio. Un equipo añade un campo a un cuerpo de petición, lo marca obligatorio porque la nueva regla de negocio lo necesita, y publica — tras lo cual cada cliente existente, ninguno de los cuales envía el campo, empieza a recibir 400. Las versiones seguras son aburridas: añade el campo como opcional con un valor por defecto en el servidor, o exígelo solo en un endpoint nuevo. La segunda trampa es la retirada de valores de enumeración: un valor de estado, una categoría, un orden de clasificación se retira porque la nueva interfaz ya no lo ofrece, mientras los datos almacenados de clientes, las aplicaciones móviles en versiones viejas y las integraciones de socios siguen enviándolo durante años. Los valores de enumeración cuestan casi nada de seguir aceptando; retirar uno debería tratarse como eliminar un endpoint.

Los cambios de tipo son la tercera trampa, y vienen en disfraces de estrechamiento y ensanchamiento. El estrechamiento —de cadena a enumeración, de entero a un rango menor, de objeto libre a forma fija— rechaza entradas que antes eran válidas y es incompatible a simple vista. El ensanchamiento es más sutil: cambiar un campo de respuesta de cadena a cadena-o-nulo, o un id entero a un id de cadena, parece generoso y rompe cada analizador estricto y SDK generado construido para el tipo antiguo. En el lado de la respuesta hay una trampa espejo: un campo que siempre estaba presente pasa a ser opcional. Los clientes lo leen incondicionalmente; la primera respuesta que lo omite es un puntero nulo en el código de otro.

La cuarta trampa es semántica, y ningún comparador puede atraparla: el campo conserva su nombre y su tipo y cambia su significado. Un precio que cambia en silencio de céntimos a dólares, una marca de tiempo que pasa de UTC a hora local, una lista que estaba ordenada y deja de estarlo — el diff de la especificación está vacío y cada cliente está equivocado. Las rupturas semánticas son la razón de que el diff mecánico sea un suelo, no un techo, para la revisión: despeja los cambios que no pueden romper a nadie, para que la atención humana se concentre en los cambios que simplemente podrían.

Revisar una subida de especificación de un proveedor antes de desplegar

Los proveedores publican nuevas versiones de especificación constantemente, y «subir el SDK y redesplegar» es como las rupturas silenciosas llegan a producción. Una revisión disciplinada lleva quince minutos. Primero, consigue ambos documentos —la especificación contra la que tu despliegue corre hoy en realidad, y la candidata— y compáralos mecánicamente en lugar de leer un registro de cambios, porque los registros de cambios son documentos de marketing: enumeran funciones, no eliminaciones. OpenAPI Diff produce esa lista localmente, lo que importa cuando la API es interna y su especificación no debería pegarse en la herramienta alojada de un extraño.

Segundo, recorre la lista de incompatibles como una lista de verificación contra tu propio uso. La mayoría de las rupturas del proveedor son teóricas para ti: un endpoint eliminado que nunca llamas, un campo obligatorio nuevo en un recurso que nunca creas. Las entradas peligrosas son las que se cruzan con tu tráfico — por eso la revisión necesita tu inventario de llamadas, no solo el diff. Tercero, comprueba la superficie no documentada: los estados por los que bifurcas, las formas de error que analizas, las cabeceras que lees. El mismo hábito que protege a los consumidores de webhooks —fijar y probar el contrato de la carga, como en las firmas de webhook, explicadas— se aplica a cualquier contrato que controle otro.

Por último, despliega por etapas. Apunta un cliente de preproducción a la nueva versión, reproduce peticiones de producción grabadas (una captura saneada, tratada como prescribe la guía HAR) y compara las respuestas antes que cualquier usuario. Si el proveedor ofrece una ventana de obsolescencia o un pin de versión, tómalo: el cambio incompatible más barato es el que adoptas a tu ritmo, cuando tus clientes están listos, y no el del día de publicación del proveedor.

Estrategias de versionado: URL, cabecera o no romper nunca

La respuesta clásica al cambio incompatible es un identificador de versión, y la discusión clásica es dónde ponerlo. El versionado por URL (/v1/, /v2/) es explícito, amable con la caché, trivialmente enrutable y visible en cada línea de registro; su coste es que versiona toda la API a la vez, así que un endpoint incompatible arrastra a cada cliente por una migración. El versionado por cabecera —una cabecera de versión o un esquema de negociación de contenido con fecha— mantiene las URL limpias y puede versionar recursos individuales, al precio de la invisibilidad: la versión desaparece de los registros, los enlaces y las barras del navegador, y depurar se vuelve arqueología. Ninguno está mal; la industria simplemente paga el coste de la URL de mejor gana porque la operabilidad vence a la elegancia a las 3 de la mañana.

La estrategia más profunda es necesitar versiones rara vez. La evolución solo aditiva —nunca eliminar, nunca exigir, nunca cambiar tipos; solo añadir cosas opcionales— mantiene viva una única versión indefinidamente, que es como de hecho operan las API públicas más longevas. Exige disciplina desde el principio: tolerancia generosa en la entrada, ids tipados como cadenas desde el primer día (para que el rango pueda crecer), enumeraciones diseñadas para extenderse y un proceso de obsolescencia que avisa mucho antes de retirar nada. El versionado semántico, codificado en semver.org, aporta la capa de señalización que la propia especificación de la OpenAPI Initiative deja deliberadamente a los editores: una subida mayor anuncia una ruptura, y los clientes pueden fijarse contra ella.

Cualquiera que sea la estrategia elegida, solo funciona como contrato social respaldado por mecánica: las versiones deben correr realmente en paralelo durante una ventana declarada, las obsolescencias deben anunciarse donde los autores de clientes las vean (registro de cambios, cabeceras de respuesta, aviso directo a los grandes consumidores) y el uso de la versión antigua debe medirse para que «ya nadie la usa» sea un hecho, no una esperanza. Una versión que se publica pero no puede operar en paralelo no es una estrategia; es un cambio de nombre de la ruptura.

Dónde encaja el comparador

Un comparador mecánico se gana su sitio en dos momentos. En la revisión, convierte «¿se rompió algo?» de una tarde de lectura lado a lado en una lista que se puede tachar — y, igual de útil, prueba un negativo: cuando una versión afirma ser solo aditiva, una lista de incompatibles vacía es evidencia, no sensaciones. En la integración continua, la misma comparación puede correr en cada solicitud de cambios que toque una especificación, fallando la construcción cuando un cambio incompatible llega sin la subida de versión o la aprobación que exige la política del proyecto. La tabla de clasificación es la política; la herramienta simplemente se niega a dejar que se olvide a la hora de la entrega.

Sus límites definen el resto del flujo de trabajo. El comparador lee solo el contrato declarado: no puede ver el comportamiento, el rendimiento, la semántica ni las peculiaridades no documentadas de las que dependen los clientes, y resume los esquemas patológicamente profundos en lugar de expandirlos para siempre. Así que la puerta completa es una canalización, no una página: diff mecánico para el contrato, pruebas de contrato que reproducen llamadas reales de clientes para el comportamiento, despliegue por etapas para la semántica y supervisión para el día en que los tres pasen algo por alto. Usado así, el diff es la capa más barata de las cuatro: quince segundos que eliminan la clase de apagón más embarazosa, aquella en la que la ruptura estaba escrita en la especificación todo el tiempo y nadie comparó los documentos.

Ese es el hábito que este artículo defiende en realidad. Las especificaciones son baratas de guardar y más baratas aún de comparar; los incidentes no. Guarda cada versión publicada de cada contrato que poseas o consumas, compara cada candidata contra la versión en producción antes de que se publique, y trata la lista de incompatibles como la agenda real de la versión. Los equipos que hacen esto dejan de discutir si un cambio «debería haber» roto a alguien — tienen la lista, fechada y archivada, de antes de que se publicara.

Fuentes y lecturas adicionales

Preguntas frecuentes

¿Añadir un campo obligatorio es siempre incompatible?

Sí, en cualquier endpoint que los clientes existentes ya llamen: las peticiones que ayer eran válidas hoy son rechazadas, que es la definición de incompatible. Las salidas estándar son añadir el campo como opcional con un valor por defecto en el servidor, exigirlo solo en un endpoint o versión nuevos, o anunciar una ventana de obsolescencia durante la cual el campo se acepta pero con aviso antes de volverse obligatorio.

¿Versionado por URL o por cabecera: cuál es mejor?

Para la mayoría de los equipos, el de URL: es visible en registros y enlaces, fácil de enrutar y cachear, y honesto sobre lo que es. El de cabecera conviene a plataformas que necesitan granularidad por recurso y ya tienen una observabilidad fuerte. Ambos vencen a la peor opción, que es el cambio incompatible sin versión. La mejor estrategia de todas es la evolución solo aditiva, que convierte el versionado en un evento raro en lugar de un impuesto rutinario.

¿Puede una herramienta atrapar cada cambio incompatible?

No. Un comparador atrapa todo lo que la especificación declara: eliminaciones, entradas obligatorias nuevas, cambios de tipo, retiradas de enumeraciones. No puede atrapar los cambios semánticos —mismo campo, nuevo significado— ni el comportamiento que la especificación nunca declaró. Por eso el diff mecánico pertenece a una canalización con pruebas de contrato y despliegue por etapas, no a un pedestal propio.