¿Por qué versionar una API y qué significa “compatibilidad hacia atrás”?
Versionar una API es establecer una forma explícita de evolucionar su contrato sin romper a los clientes existentes. La compatibilidad hacia atrás (backward compatibility) significa que un cliente que funciona hoy seguirá funcionando mañana sin cambios, aunque el servidor haya evolucionado. En la práctica, esto se logra controlando cuidadosamente qué cambios se introducen, cómo se publican y durante cuánto tiempo se soportan versiones antiguas.
Estrategias de versionado en APIs REST
1) Versionado en la URL (path versioning)
Consiste en incluir la versión en la ruta, por ejemplo /v1/... y /v2/.... Es simple de entender, fácil de probar y de enrutar en gateways y balanceadores.
- Ejemplo:
GET /v1/orders/123vsGET /v2/orders/123 - Ventajas: claridad, cachés y logs distinguen versiones, fácil de documentar.
- Desventajas: la URL deja de representar solo el recurso; puede incentivar “forks” grandes entre versiones si no se gobierna bien.
- Cuándo usarlo: APIs públicas con muchos consumidores, cuando se necesita una separación fuerte y visible entre versiones o cuando la infraestructura favorece el ruteo por path.
2) Versionado por cabecera Accept con media types versionados
Se negocia la versión mediante Accept (y opcionalmente Content-Type para el cuerpo de la petición). El endpoint puede mantenerse estable (misma URL) y el contrato se selecciona por media type.
- Ejemplo de petición:
GET /orders/123conAccept: application/vnd.acme.orders+json;version=2 - Respuesta:
Content-Type: application/vnd.acme.orders+json;version=2 - Ventajas: la URL permanece “limpia” y centrada en el recurso; permite coexistencia de representaciones.
- Desventajas: más complejo para clientes simples, herramientas y cachés; requiere disciplina en gateways y documentación.
- Cuándo usarlo: cuando se quiere mantener URLs estables, cuando hay múltiples representaciones del mismo recurso, o cuando se controla el ecosistema de clientes (B2B con SDKs, por ejemplo).
3) Versionado semántico del contrato (sin forzar versión en cada request)
Aquí la API se gobierna con reglas de compatibilidad y un esquema de versionado semántico del contrato (por ejemplo, MAJOR.MINOR.PATCH). La idea es: cambios compatibles se despliegan sin obligar a los clientes a cambiar de “v1 a v2” inmediatamente; solo cambios breaking incrementan el MAJOR y suelen requerir una nueva versión expuesta (por URL o media type).
- MAJOR: cambios breaking (requieren migración).
- MINOR: cambios compatibles (nuevos campos opcionales, nuevos endpoints, nuevos valores tolerados).
- PATCH: correcciones sin impacto en contrato (bugs, performance, documentación).
Este enfoque no reemplaza necesariamente a URL o Accept: los complementa. Por ejemplo, puedes exponer /v1 y dentro de v1 evolucionar con MINOR/PATCH sin romper clientes.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Criterios para elegir una estrategia
| Criterio | URL | Accept (media type) | SemVer del contrato |
|---|---|---|---|
| Facilidad para consumidores | Alta | Media | Alta (si no hay cambios breaking frecuentes) |
| Soporte en infraestructura (routing, gateways) | Muy alto | Variable | N/A (es gobernanza) |
| Coexistencia de múltiples representaciones | Media | Alta | Alta (si se diseña con compatibilidad) |
| Visibilidad en logs/monitoring | Alta | Media | Depende de cómo se exponga |
| Riesgo de fragmentación (“forks”) | Medio | Medio | Bajo (si se respeta compatibilidad) |
Regla práctica: si tu API es pública y heterogénea, el versionado en URL suele ser la opción más pragmática. Si necesitas negociación de representaciones o quieres URLs estables, usa Accept. En cualquier caso, gobierna la evolución con SemVer del contrato y políticas de compatibilidad.
Qué constituye un cambio breaking (y por qué)
Un cambio breaking es aquel que puede hacer que un cliente existente falle (errores de parseo, validación, lógica de negocio) sin haber cambiado su código. Ejemplos típicos:
- Eliminar un campo o renombrarlo.
- Cambiar el tipo de un campo (por ejemplo, de número a string) o su formato de manera incompatible.
- Hacer obligatorio un campo que antes era opcional.
- Cambiar el significado de un campo (misma forma, distinta semántica).
- Eliminar o cambiar valores posibles de un enum que los clientes esperan.
- Cambiar reglas de validación de forma más estricta sin transición (por ejemplo, antes aceptaba
null, ahora no). - Cambiar estructura de la respuesta (envoltorios, listas vs objeto, etc.).
- Modificar comportamiento de manera que rompa suposiciones (por ejemplo, orden, consistencia, o condiciones de error) aunque el esquema sea igual.
Importante: algo puede ser “compatible a nivel de JSON” pero breaking a nivel de negocio. Por ejemplo, un campo status que antes significaba “pagado” y ahora significa “confirmado” rompe integraciones aunque el tipo sea el mismo.
Cómo evitar cambios breaking: patrones de compatibilidad
1) Añadir campos opcionales (evolución aditiva)
La forma más segura de evolucionar respuestas es añadir campos nuevos como opcionales, manteniendo los existentes. Los clientes antiguos los ignorarán si están bien implementados.
// Antes (v1):{ "id": "123", "total": 49.90 }// Después (compatible):{ "id": "123", "total": 49.90, "currency": "EUR" }Buenas prácticas:
- No cambies el significado de campos existentes; agrega uno nuevo si necesitas una semántica distinta.
- Evita introducir campos obligatorios de golpe; primero opcional, luego migración, luego (si aplica) major version.
2) Tolerancia a campos desconocidos (robustez del cliente)
Para que la evolución aditiva funcione, los clientes deben tolerar campos que no conocen. Esto es una regla de oro: los clientes deben ignorar propiedades desconocidas en respuestas.
- En SDKs y deserializadores, habilita “ignore unknown properties”.
- En validaciones JSON Schema del lado cliente, evita
additionalProperties: falsesalvo que controles estrictamente versiones y despliegues.
Del lado servidor, también conviene tolerar campos desconocidos en requests cuando sea seguro, especialmente en endpoints de actualización parcial, para facilitar despliegues progresivos (clientes nuevos enviando campos que servidores antiguos aún no usan).
3) Deprecación gradual (sin cortar de inmediato)
Cuando un campo o comportamiento debe desaparecer, no lo elimines de inmediato. Depreca en fases:
- Fase 1: anunciar deprecación y ofrecer alternativa.
- Fase 2: mantener ambos (viejo y nuevo) en paralelo.
- Fase 3: marcar el viejo como “no recomendado” y medir uso.
- Fase 4: retirar en una versión mayor (MAJOR) o tras ventana de soporte.
Ejemplo: reemplazar fullName por firstName y lastName. Durante la transición, puedes devolver ambos, y aceptar ambos en request (priorizando el nuevo si ambos aparecen).
// Respuesta transicional:{ "id": "u1", "fullName": "Ada Lovelace", "firstName": "Ada", "lastName": "Lovelace" }Políticas de ciclo de vida (lifecycle) para versiones
Anuncios y comunicación
Define una política pública y repetible. Por ejemplo:
- Notificación de cambios compatibles (MINOR) con antelación razonable (changelog y notas de versión).
- Notificación de deprecaciones con fecha de retiro (sunset date).
- Canales: portal de desarrolladores, mailing list, webhook de cambios, o feed de changelog.
Headers de deprecación y retiro
Usa cabeceras para que los clientes detecten deprecaciones automáticamente. Un patrón común:
Deprecation: true(o una fecha) para indicar que el recurso/versión está deprecado.Sunset: Wed, 01 Oct 2026 00:00:00 GMTpara indicar cuándo se retirará el soporte.Link: <https://api.acme.com/docs/migrations/v1-to-v2>; rel="deprecation"para apuntar a guía de migración.
HTTP/1.1 200 OKContent-Type: application/jsonDeprecation: trueSunset: Wed, 01 Oct 2026 00:00:00 GMTLink: <https://api.acme.com/docs/migrations/v1-to-v2>; rel="deprecation"Ventanas de soporte
Define cuánto tiempo soportas cada versión mayor. Ejemplo de política:
- Se soportan como máximo 2 versiones mayores en paralelo.
- Cada versión mayor tiene 12 meses de soporte desde el lanzamiento de la siguiente mayor.
- Parcheo de seguridad para la versión anterior durante 6 meses adicionales.
Lo importante no es el número exacto, sino que sea predecible y que se aplique consistentemente.
Migración con compatibilidad progresiva (strangler / dual-run)
La migración más segura evita “big bang”. Se busca que clientes y servidores puedan convivir mientras se despliega el cambio.
Guía práctica paso a paso: introducir una v2 sin romper v1
Paso 1: clasifica el cambio (¿breaking o compatible?)
Antes de decidir “v2”, determina si puedes resolverlo con evolución aditiva. Si necesitas eliminar/renombrar/cambiar semántica, asúmelo como breaking y planifica versión mayor.
Paso 2: elige el mecanismo de exposición de versión
- Si usarás URL: define
/v1y/v2. - Si usarás Accept: define media types versionados y reglas de negociación.
Ejemplo con URL:
GET /v1/orders/123GET /v2/orders/123Ejemplo con Accept:
GET /orders/123Accept: application/vnd.acme.orders+json;version=1GET /orders/123Accept: application/vnd.acme.orders+json;version=2Paso 3: diseña v2 para permitir transición
Incluye mecanismos para que un cliente pueda migrar gradualmente:
- Si cambias un campo, considera exponer ambos durante un tiempo en v2 (o proveer un endpoint de “compat view”).
- Si cambias un enum, permite valores antiguos como alias durante la ventana de migración.
- Si cambias estructura, considera un modo de respuesta compatible mediante parámetro o media type alternativo (solo si lo gobiernas estrictamente para no crear combinaciones infinitas).
Paso 4: implementa “dual write / dual read” cuando aplique
Si el cambio afecta persistencia o eventos internos, una táctica común es:
- Dual write: al escribir, llenar tanto el campo viejo como el nuevo.
- Dual read: al leer, preferir el nuevo si existe; si no, derivarlo del viejo.
// Pseudológica en servidor:// Al guardar:if (req.firstName && req.lastName) { store.firstName=req.firstName; store.lastName=req.lastName; store.fullName=req.firstName+" "+req.lastName; } else if (req.fullName) { store.fullName=req.fullName; // opcional: intentar split heurístico o dejar first/last vacíos }Paso 5: publica deprecación de v1 con headers y documentación de migración
En respuestas de v1, añade cabeceras de deprecación y un enlace a la guía de migración. Además, registra métricas de uso por cliente (API key, OAuth client, etc.) para priorizar soporte.
Paso 6: ofrece ejemplos de migración y pruebas de compatibilidad
Incluye ejemplos concretos de requests/responses y una matriz de compatibilidad. Ejemplo: cambio de status de PAID/UNPAID a paymentStatus con valores más expresivos.
// v1:{ "id": "o1", "status": "PAID" }// v2 (nuevo campo, viejo aún presente durante transición):{ "id": "o1", "status": "PAID", "paymentStatus": "SETTLED" }Regla de transición: durante la ventana, status se mantiene y se deriva de paymentStatus para clientes v1; en v2 se recomienda usar paymentStatus.
Paso 7: retira v1 al final de la ventana de soporte
Al llegar la fecha Sunset, puedes:
- Responder con un error indicando versión retirada y link a migración.
- Bloquear por cliente si necesitas excepciones temporales (con acuerdos explícitos), evitando extender indefinidamente el soporte general.
HTTP/1.1 410 GoneContent-Type: application/jsonLink: <https://api.acme.com/docs/migrations/v1-to-v2>; rel="alternate"{ "error": "version_retired", "message": "v1 is no longer supported. Migrate to v2." }Compatibilidad progresiva: escenarios comunes y cómo resolverlos
Escenario A: necesitas renombrar un campo
- Evita: renombrar directamente en la misma versión.
- Haz: agrega el nuevo campo, mantén el viejo, documenta prioridad, depreca el viejo, retira en MAJOR.
Escenario B: necesitas cambiar el tipo de un campo
Ejemplo: total pasa de número a string con moneda. En lugar de cambiar el tipo:
- Introduce
totalMoneycomo objeto (amount,currency) o como string formateado. - Mantén
totalcomo número durante la transición.
// Compatible:{ "total": 49.90, "totalMoney": { "amount": "49.90", "currency": "EUR" } }Escenario C: necesitas endurecer validaciones
Si vas a rechazar valores que antes aceptabas:
- Primero, emite warnings (por ejemplo, en logs o cabeceras informativas) cuando detectes valores “legacy”.
- Luego, introduce la validación estricta en una versión mayor o tras una ventana con feature flag por cliente.
Escenario D: cambios en enums
Los enums son una fuente frecuente de roturas. Reglas:
- Los clientes deben tolerar valores desconocidos (fallback a “OTHER/UNKNOWN”).
- El servidor puede añadir valores nuevos sin romper si los clientes no asumen exhaustividad.
- Eliminar valores o cambiar significado es breaking: planifica MAJOR y transición con alias.
Checklist operativo de versionado y compatibilidad
- Define qué es breaking en tu organización (lista explícita) y úsala en revisiones.
- Obliga a que clientes ignoren campos desconocidos y toleren enums desconocidos.
- Prefiere cambios aditivos: nuevos campos opcionales, nuevos endpoints, nuevas representaciones.
- Depreca con fechas:
Deprecation,SunsetyLinka migración. - Establece ventanas de soporte y cúmplelas.
- Mide adopción por versión y por cliente para gestionar excepciones.
- Para migraciones complejas, usa dual read/dual write y despliegue progresivo.