El contrato de la v1 de Tienda Aroma está prácticamente cerrado: recursos, métodos, códigos, representaciones y colecciones. Y justo cuando un contrato se publica empieza el problema de verdad: el negocio cambia y la API tiene que cambiar con él, sin romper a nadie. Aroma Móvil tiene versiones instaladas en teléfonos que nadie va a actualizar en meses; RápidoEnvíos tiene una integración escrita hace un año que funciona y que nadie quiere tocar. Esta lección enseña a distinguir con precisión qué cambios se pueden hacer sin avisar y cuáles no, compara las cinco estrategias de versionado que se usan en el sector, justifica la de Tienda Aroma y diseña el ciclo completo de deprecación, desde el anuncio hasta el apagado.

Contenido

  1. Por qué versionar es un último recurso
  2. Cambios retrocompatibles frente a rompedores
  3. Tabla de cambios sobre la API de Tienda Aroma
  4. Estrategias de versionado
  5. Comparativa y decisión de Tienda Aroma
  6. SemVer aplicado a APIs y sus límites
  7. Convivencia de versiones
  8. Ciclo de vida y deprecación
  9. Cabeceras Deprecation, Sunset y Warning
  10. Estrategias para no tener que versionar

  1. Por qué versionar es un último recurso

Publicar una v2 suena a progreso. En realidad es una factura: dos bases de código que mantener, dos documentaciones, dos conjuntos de pruebas, dos superficies de seguridad, y una migración que hay que negociar con cada consumidor. Empresas conocidas llevan una década manteniendo su v1 porque apagarla resultó imposible.

Por eso la regla número uno del versionado es: evita necesitarlo. La mayoría de los cambios se pueden hacer de forma retrocompatible si el contrato se diseñó con tolerancia a la evolución (02-01) y si sabes distinguir con precisión qué rompe y qué no. Eso es exactamente lo que hace la sección siguiente.

  1. Cambios retrocompatibles frente a rompedores

La definición operativa, y la única que sirve:

Un cambio es retrocompatible si un cliente escrito contra el contrato anterior, y que se comportaba correctamente, sigue funcionando sin tocar una línea.

Fíjate en el matiz "que se comportaba correctamente": un cliente que se rompe porque recorría los campos del JSON asumiendo que eran exactamente cinco no es culpa tuya, siempre que la documentación advirtiera de que pueden aparecer campos nuevos. De ahí que el principio de robustez y el tolerant reader de 02-01 no sean un consejo blando: son la condición que hace posible evolucionar sin versionar.

2.1. Regla general

  • Añadir es casi siempre seguro (campos, endpoints, valores de enumerado de salida, parámetros opcionales).
  • Quitar, renombrar o restringir casi siempre rompe (campos, endpoints, valores admitidos, códigos de estado).

2.2. La asimetría entrada/salida

Es lo que más confunde, y merece pensarlo despacio: añadir un valor a un enumerado no es lo mismo en la respuesta que en la petición.

  • En la salida (respuestas del servidor): añadir estado: "devuelto" es un cambio potencialmente rompedor para clientes que hacen un switch sin caso por defecto. Se considera aceptable solo porque la documentación avisa desde el día uno de que los enumerados crecen.
  • En la entrada (peticiones del cliente): aceptar un valor nuevo en tueste es totalmente seguro, porque nadie lo estaba enviando antes.

La simétrica, y por la misma razón invertida: relajar una validación de entrada es seguro; endurecerla rompe. Si hoy aceptas comentarios de 5.000 caracteres y mañana los limitas a 500, hay clientes que funcionaban y dejan de funcionar.

  1. Tabla de cambios sobre la API de Tienda Aroma

Cambio ¿Rompe? Por qué
Añadir el campo puntuacionMedia a la respuesta de café No Los clientes que lo ignoran siguen igual (tolerant reader)
Añadir el endpoint /v1/suscripciones No Nadie lo llamaba
Añadir el filtro opcional ?disponible=true No Comportamiento sin el parámetro, intacto
Añadir el enlace devolver en _links de un pedido No Aditivo, y ya avisamos de que los enlaces dependen del estado
Aceptar un valor nuevo en tueste de entrada No Nadie enviaba natural antes
Añadir el valor devuelto a estado de pedido Casi: documentado como esperable Rompe a quien no previó valores nuevos; se anuncia con antelación
Renombrar precio a precioEuros Todo cliente que lea precio recibe undefined
Eliminar el campo stock de la respuesta Desaparece un dato que se estaba usando
Cambiar stock de número a cadena ("120") stock > 0 deja de comportarse igual; "0" es verdadero en JavaScript
Cambiar precioEuros de euros a céntimos (1450) Sí, y del peor tipo No falla: muestra precios cien veces mayores. Un cambio silencioso es peor que uno ruidoso
Hacer obligatorio el campo origen al crear un café Peticiones que funcionaban ahora dan 400
Limitar comentario de 5.000 a 500 caracteres Endurecer una validación rompe a quien estaba en el margen
Eliminar el valor pendiente_pago de estado Los clientes lo tienen en sus condicionales
Cambiar POST /pedidos de 201 a 200 Los clientes que comprueban === 201 fallan; además desaparece Location
Cambiar el codigo de error cafe_no_encontrado a no_encontrado_cafe El codigo es contrato; se compara en el código del cliente (02-04)
Cambiar el texto de mensaje de un error No Se documentó que mensaje es para humanos y puede cambiar
Cambiar el orden por defecto de /cafes de nombre a -fechaCreacion Aunque parezca inocuo, cambia lo que ve la página 1 y estaba documentado
Reducir limite máximo de 100 a 50 Peticiones válidas empiezan a devolver 400
Aumentar limite máximo de 100 a 200 No Relajar un límite es seguro
Cambiar el formato de id de caf_001 a caf_01HQ8ZK… No Se documentó como cadena opaca (02-02); rompe solo a quien lo parseaba, que estaba avisado
Devolver null en un campo que nunca lo era El cliente hace cafe.origen.toUpperCase() y revienta
Migrar /cafes de offset a cursor eliminando desplazamiento Un parámetro publicado no se retira sin versión
Añadir la cabecera Aroma-RateLimit-Restantes No Las cabeceras nuevas se ignoran solas
Corregir un 500 que ahora devuelve 400 No (se considera arreglo) Estabas incumpliendo tu propio contrato

La última fila señala una zona gris real: arreglar un bug puede romper a quien dependía del bug. Se documenta en el changelog, se anuncia, y en general se considera cambio no rompedor. Pero si el comportamiento erróneo llevaba dos años ahí, quizá se haya convertido de facto en contrato: hay que mirarlo caso por caso.

  1. Estrategias de versionado

4.1. Versión en la ruta

https://api.tiendaaroma.example/v1/cafes
https://api.tiendaaroma.example/v2/cafes

Es la más usada del sector: Twitter/X, GitHub (durante años), Stripe en su URL base, casi todas las APIs corporativas.

# Visible, copiable, probable desde un navegador
curl https://api.tiendaaroma.example/v1/cafes

A favor: visible a simple vista; se prueba con un navegador o con curl sin cabeceras; el enrutado es trivial (un prefijo); los logs y las métricas separan versiones sin esfuerzo; los ejemplos de la documentación son autocontenidos.

En contra: puristas de REST objetan que la URI debería identificar el recurso, no su formato, y que /v1/cafes y /v2/cafes son "el mismo café" con dos URIs distintas; además, versiona la API entera aunque solo cambie un recurso, y los enlaces _links guardados por los clientes quedan clavados a una versión.

4.2. Versión en query param

https://api.tiendaaroma.example/cafes?version=2

A favor: la URI base es única; es fácil de probar; se puede poner un valor por defecto.

En contra: se mezcla con los parámetros de negocio (filtros, orden, paginación), se pierde con facilidad al copiar URLs, complica la caché y hace ambiguo qué versión se sirve si el parámetro falta.

4.3. Versión en cabecera personalizada

GET /cafes HTTP/1.1
Aroma-Version: 2

A favor: las URIs quedan limpias y estables; permite versionar de forma granular.

En contra: invisible. No se puede pegar un enlace en un ticket y esperar que reproduzca el problema; probar en un navegador es imposible sin herramientas; las cachés necesitan Vary: Aroma-Version y muchos gateways lo ignoran; y hay que decidir qué pasa si no se envía.

4.4. Versión en el media type

GET /cafes HTTP/1.1
Accept: application/vnd.tiendaaroma.v2+json

Es la opción más correcta desde el punto de vista de REST: la versión pertenece a la representación, y la representación se negocia con Accept (02-05). GitHub la usó durante años (application/vnd.github.v3+json).

A favor: teóricamente impecable; usa un mecanismo estándar de HTTP; permite versionar recurso a recurso.

En contra: la más difícil de usar. Nadie recuerda la cadena de memoria; curl requiere cabecera explícita; muchas herramientas y clientes generados no lo manejan bien; e igual que la cabecera personalizada, es invisible en la URL.

4.5. Versionado por fecha

GET /cafes HTTP/1.1
Aroma-Fecha-Version: 2026-03-14

Es el estilo de Stripe: cada cuenta queda anclada a la versión vigente el día que se integró, y el servidor aplica transformaciones encadenadas para adaptar la respuesta actual a la forma que esperaba aquella fecha.

A favor: no hay saltos traumáticos de v1 a v2; los cambios se introducen de forma continua; cada consumidor migra cuando quiere; es el que mejor escala en APIs públicas grandes.

En contra: complejidad alta. Exige mantener una cadena de transformaciones bien probada y una disciplina de ingeniería considerable. Es un patrón excelente para una empresa cuyo producto es la API, y desproporcionado para casi todas las demás.

  1. Comparativa y decisión de Tienda Aroma

Criterio Ruta Query Cabecera Media type Fecha
Visibilidad Alta Alta Baja Baja Baja
Facilidad de prueba (curl, navegador) Muy alta Alta Baja Muy baja Baja
Pureza REST Baja Baja Media Alta Media
Granularidad (por recurso) Baja Baja Alta Alta Alta
Facilidad de enrutado y despliegue Muy alta Media Media Baja Baja
Caché e intermediarios Sencilla Media Requiere Vary Requiere Vary Requiere Vary
Complejidad de implementación Baja Baja Media Media Alta
Migración progresiva Baja Baja Media Media Muy alta
Quién la usa Twitter/X, la mayoría APIs sencillas Azure, algunas GitHub (histórico) Stripe

Decisión de Tienda Aroma: versión en la ruta (/v1).

Las razones, en orden de peso:

  1. Visibilidad y soporte. Cuando RápidoEnvíos abra una incidencia y pegue una URL, sabremos exactamente contra qué está hablando. Con cabeceras, la mitad de los tickets empiezan con "¿qué versión estabas usando?".
  2. Coste de implementación y operación. Un prefijo de ruta se enruta, se despliega, se mide y se apaga con herramientas estándar. Con cuatro consumidores y un equipo pequeño, la pureza teórica no compensa.
  3. Docencia y documentación. Todos los ejemplos curl de la documentación funcionan copiados y pegados, sin cabeceras ocultas.
  4. Coherencia con lo ya decidido. La base URL con /v1 está fijada desde el módulo 1 y publicada a los cuatro consumidores.

Y las decisiones asociadas, que también son contrato:

  • Solo se versiona el número mayor: /v1, /v2. Nunca /v1.2. Los cambios menores son retrocompatibles por definición y no necesitan URL nueva.
  • La versión es obligatoria en la ruta. No existe https://api.tiendaaroma.example/cafes sin versión que "apunte a la última": un cliente que no elige versión acaba roto el día que la última cambia.
  • Todos los recursos comparten versión. Sube toda la API a la vez, aunque solo cambien los pedidos. Simplifica el razonamiento a cambio de algo de granularidad.

  1. SemVer aplicado a APIs y sus límites

SemVer (versionado semántico) define MAYOR.MENOR.PARCHE:

Componente Cuándo sube Ejemplo en Tienda Aroma
MAYOR Cambio rompedor Renombrar precio a precioEuros
MENOR Funcionalidad nueva, retrocompatible Añadir /v1/suscripciones
PARCHE Corrección sin cambio de contrato Arreglar un cálculo de IVA

Aplicado a una API HTTP, SemVer tiene tres límites que conviene tener claros:

  1. Solo el número mayor aparece en la URL. A un consumidor le da igual si está usando la 1.4.2 o la 1.7.0: el contrato que ve es el mismo. Menor y parche viven en el changelog, no en la ruta.
  2. No hay "instalación" que fijar. Con una biblioteca, el consumidor decide cuándo actualizar; con una API alojada, el servidor actualiza para todos a la vez. Por eso los cambios menores tienen que ser rigurosamente retrocompatibles: no hay marcha atrás para el cliente.
  3. La frontera mayor/menor se negocia. Añadir un valor de enumerado en la salida (sección 2.2) es, técnicamente, potencialmente rompedor; declararlo MAYOR obligaría a publicar una v2 cada trimestre. Se documenta como esperable y se trata como MENOR con anuncio previo. Escribe esa política en la guía de estilo, porque es la decisión que más discusiones evita.

Tienda Aroma mantiene, por tanto, dos numeraciones: la versión de la URL (v1) para el contrato, y la versión semántica interna (1.7.0) en el changelog y en el campo info.version de OpenAPI (02-08).

  1. Convivencia de versiones

Cuando llega la v2, las dos conviven un tiempo. Las preguntas prácticas:

¿Cuántas versiones mantener?

Dos como máximo: la actual y la anterior en deprecación. Tres versiones vivas es señal de que la migración anterior nunca terminó, y el coste crece más que linealmente: cada corrección de seguridad y cada cambio de negocio hay que aplicarlo en todas.

¿Cuánto cuesta realmente?

Coste Detalle
Código Rutas, transformaciones y, a veces, lógica de negocio duplicada
Pruebas Toda la batería, dos veces (03-08)
Documentación Dos referencias completas y coherentes
Soporte El doble de casuística en cada incidencia
Seguridad Cada parche, aplicado y verificado dos veces
Datos La v1 puede necesitar campos que la v2 ya no usa

¿Cómo se enrutan?

El patrón habitual, y el que usará Tienda Aroma en el módulo 3: una sola aplicación con dos capas de presentación sobre una lógica de negocio compartida.

graph TD
    C1["Aroma Móvil 3.x"] --> R{"Enrutado<br/>por prefijo"}
    C2["SPA de la tienda"] --> R
    C3["RápidoEnvíos"] --> R
    R -->|"/v1/*"| V1["Capa v1<br/><i>transforma al contrato antiguo</i>"]
    R -->|"/v2/*"| V2["Capa v2<br/><i>contrato actual</i>"]
    V1 --> N["Lógica de negocio<br/>y datos<br/><b>compartidos</b>"]
    V2 --> N

La clave es no duplicar la lógica de negocio. La v1 se implementa como una capa de adaptación sobre el modelo actual: renombra campos, recorta lo que no existía, calcula lo que se eliminó. Duplicar el servicio entero garantiza que las dos versiones diverjan en comportamiento y que aparezcan bugs que solo ocurren en una.

Cuando la transformación deja de ser posible —porque el modelo de datos cambió de verdad— es la señal de que la v1 debe apagarse, no de que haya que duplicar el sistema.

  1. Ciclo de vida y deprecación

Toda versión recorre cuatro fases:

graph LR
    A["<b>Vigente</b><br/>versión recomendada"] --> B["<b>Deprecada</b><br/>funciona, pero avisa"]
    B --> C["<b>Sunset anunciado</b><br/>fecha de apagado fijada"]
    C --> D["<b>Apagada</b><br/>410 Gone"]

El calendario de Tienda Aroma, escrito en la documentación antes de publicar la v1 —porque las condiciones de retirada se anuncian al principio, no cuando ya molestan—:

Hito Plazo Qué ocurre
Publicación de la v2 Día 0 La v1 sigue vigente y soportada
Deprecación de la v1 Día 0 Cabecera Deprecation en todas las respuestas v1; changelog y correo a los consumidores
Recordatorios Meses 3, 6, 9, 11 Correo a los consumidores que siguen llamando, con sus cifras de uso
Solo lectura (opcional) Mes 11 Las escrituras en v1 devuelven 410; las lecturas siguen
Apagado Mes 12 Toda la v1 responde 410 Gone con enlace a la guía de migración

Mínimo de 12 meses para consumidores externos como RápidoEnvíos. Para clientes propios (SPA, panel) el plazo puede acortarse porque controlamos el despliegue, pero no para Aroma Móvil: hay usuarios que no actualizan la app en un año, y aquí está el consumidor que de verdad marca el calendario.

Comunicación

Una deprecación que solo vive en las cabeceras HTTP es una deprecación que nadie lee. El paquete completo:

  1. Cabeceras en cada respuesta (sección 9) — para el software.
  2. Changelog con fecha, motivo y guía de migración campo a campo — para el desarrollador que investiga.
  3. Correo directo a los responsables técnicos de cada consumidor identificado — para el humano que decide.
  4. Aviso en el portal de desarrollador (05-06).
  5. Recordatorios segmentados: solo a quien sigue usando la versión antigua, con sus datos concretos de uso. Un correo genérico se ignora; "hemos registrado 12.400 llamadas vuestras a /v1 este mes" no.

Métricas: saber quién sigue ahí

No se apaga nada sin datos. Hay que medir, por versión y por consumidor:

Métrica Para qué
Peticiones por versión y día Ver la curva de migración y decidir si el plazo es realista
Peticiones por versión y cliente de API Saber a quién hay que llamar por teléfono
Endpoints v1 más usados Priorizar la guía de migración por lo que de verdad se usa
Último acceso por cliente Detectar integraciones zombis que quizá ya no importan
Errores en v2 tras migrar Descubrir que la migración va mal antes de que se queje nadie

Esto exige identificar a cada consumidor, lo que se consigue con la clave o el token de API de cada uno (04-03) y con la observabilidad de 04-07. Sin identificación de consumidores no hay deprecación posible: solo apagones a ciegas.

El apagado

Al llegar la fecha, la v1 responde:

HTTP/1.1 410 Gone
Content-Type: application/json
Link: <https://docs.tiendaaroma.example/migracion-v1-v2>; rel="deprecation"

{
  "error": {
    "codigo": "version_api_retirada",
    "mensaje": "La versión v1 de la API se retiró el 2027-03-14. Migra a /v2.",
    "detalles": [
      { "guiaMigracion": "https://docs.tiendaaroma.example/migracion-v1-v2" }
    ]
  }
}

410 Gone y no 404: el recurso existió y se ha eliminado deliberadamente (02-04). Y no se redirige v1 a v2 con un 301: los contratos son distintos, así que el cliente recibiría una respuesta con otra forma y fallaría de manera confusa. Es mejor un error claro que un éxito engañoso.

  1. Cabeceras Deprecation, Sunset y Warning

El estándar permite anunciar la retirada dentro del propio protocolo, de modo que el software pueda enterarse sin leer un correo.

HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: @1773484200
Sunset: Sun, 14 Mar 2027 00:00:00 GMT
Link: <https://api.tiendaaroma.example/v2/cafes>; rel="successor-version",
      <https://docs.tiendaaroma.example/migracion-v1-v2>; rel="deprecation"

{ "datos": [ ], "total": 137 }
Cabecera Estándar Qué dice
Deprecation RFC 9745 Que el recurso está deprecado, y desde cuándo (fecha con @ y segundos epoch, o true)
Sunset RFC 8594 Fecha exacta a partir de la cual dejará de responder, en formato de fecha HTTP
Link con rel="successor-version" RFC 8288 Dónde está el sustituto
Link con rel="deprecation" RFC 9745 Dónde está la explicación y la guía de migración
Warning RFC 7234, obsoleta Avisos legibles; retirada en la RFC 9111: no la uses en diseños nuevos

Un detalle importante y que se olvida: estas cabeceras se pueden usar sin publicar una versión nueva, sobre un recurso o un campo concreto. Si Tienda Aroma va a retirar /v1/clientes/{id}/preferencias sustituyéndolo por otra cosa, ese endpoint concreto puede llevar Deprecation y Sunset mientras el resto de la v1 sigue tan sana.

Y una recomendación práctica para los consumidores, que conviene incluir en la documentación: registrad un aviso en vuestros logs cuando lleguen estas cabeceras. Es la forma barata de enterarse de una deprecación sin depender de que alguien lea el correo adecuado.

  1. Estrategias para no tener que versionar

Volvemos a donde empezamos: la mejor versión nueva es la que no hace falta. Cinco técnicas concretas.

10.1. Campos opcionales y aditivos

Añadir en lugar de cambiar. Cuando haya que sustituir un campo, conviven los dos durante la transición:

{
  "id": "caf_001",
  "precioEuros": 14.50,
  "precio": 14.50
}

precio queda documentado como deprecado, con fecha de retirada, y desaparece en la v2. Cuesta duplicar un dato durante unos meses; ahorra una versión entera.

10.2. Tolerant reader

Es responsabilidad del consumidor, y hay que documentarla y repetirla:

// ✗ Frágil: se rompe con cualquier campo nuevo o cualquier valor nuevo
const { id, nombre, precioEuros, stock, tueste } = cafe;
switch (pedido.estado) {
  case "pendiente_pago": mostrarBotonPagar(); break;
  case "pagado":         mostrarFactura();    break;
  case "enviado":        mostrarSeguimiento(); break;
}

// ✓ Tolerante: ignora lo desconocido y tiene caso por defecto
const nombre = cafe.nombre ?? "Sin nombre";
switch (pedido.estado) {
  case "pendiente_pago": mostrarBotonPagar(); break;
  case "pagado":         mostrarFactura();    break;
  case "enviado":        mostrarSeguimiento(); break;
  default:               mostrarEstadoGenerico(pedido.estado);
}

La segunda versión sobrevive al día en que aparezca estado: "devuelto". Un cliente tolerante es lo que convierte a "añadir" en una operación segura, y por eso Tienda Aroma lo documenta como requisito de integración, no como consejo.

10.3. Feature flags y despliegue progresivo

Un comportamiento nuevo se activa primero para un consumidor concreto, se mide y se generaliza. Permite validar un cambio dudoso con la SPA (que controlamos) antes de exponerlo a RápidoEnvíos. Cuidado: una bandera que se queda para siempre es una versión encubierta; toda bandera necesita fecha de retirada.

10.4. Expansión y selección de campos

Ya diseñadas en 02-05: expandir y campos absorben buena parte de las peticiones de cambio ("necesitamos los datos del cliente dentro del pedido") sin tocar el contrato, porque el mecanismo genérico ya estaba previsto.

10.5. Recursos nuevos en lugar de recursos cambiados

Si /cafes tiene que cambiar de forma radical, a veces la respuesta correcta no es la v2 completa, sino un recurso nuevo con nombre propio (/catalogo, /productos) que convive con el antiguo, ya deprecado. Se paga con dos nombres para conceptos parecidos, y se cobra en no versionar la API entera por un solo recurso.

Errores Comunes y Consejos

  • Versionar por costumbre. Publicar v2 porque "toca" duplica el coste sin dar valor. Versiona solo cuando un cambio rompedor sea inevitable.
  • Creer que añadir un campo nunca rompe. Es cierto para clientes tolerantes; con validación estricta de esquema en el cliente, rompe. Por eso hay que documentar que la API puede añadir campos.
  • Cambiar el significado de un campo sin cambiar su nombre. El peor cambio posible: no falla, miente. precioEuros pasando a céntimos multiplica los precios por cien silenciosamente.
  • No versionar el contrato de errores. Los codigo de error son contrato tanto como los campos: renombrarlos rompe clientes (02-04).
  • Mantener tres o cuatro versiones vivas. Es un síntoma, no una virtud: significa que ninguna migración se completó.
  • Apagar sin datos ni aviso. Sin métricas por consumidor y sin plazo anunciado, el apagado es una incidencia grave con tu socio.
  • Redirigir v1 a v2 con 301. Los contratos son distintos: el cliente recibirá 200 con una forma que no espera y fallará de forma incomprensible.
  • Consejo: escribe el changelog desde el primer día. Es el artefacto más barato y el que más agradecen los consumidores (02-08).
  • Consejo: aplica el "test del cliente congelado". Ante cada cambio, pregúntate: ¿seguiría funcionando la versión de Aroma Móvil que se instaló hace un año? Si la respuesta es no, es rompedor.

Ejercicios

Ejercicio 1: clasificar cambios

Para cada cambio propuesto sobre la v1 de Tienda Aroma, indica si es retrocompatible o rompedor y justifícalo. Si es rompedor, propón una alternativa que no lo sea.

  1. Añadir puntuacionMedia y numeroResenas a la representación de café.
  2. Renombrar notasCata a notasDeCata.
  3. Añadir el estado devuelto a los pedidos.
  4. Dejar de devolver email en la representación de cliente por privacidad.
  5. Aceptar PATCH con application/json-patch+json, además de Merge Patch.
  6. Cambiar totalEuros de 29.00 a "29.00" (cadena) para evitar problemas de coma flotante.
  7. Hacer obligatorio Idempotency-Key en POST /cafes/{id}/resenas.
  8. Bajar el limite máximo de 100 a 50 por problemas de rendimiento.

Ejercicio 2: diseñar una migración

Tienda Aroma necesita soportar varias divisas. La forma actual es:

{ "precioEuros": 14.50 }

Y la deseada:

{ "precio": { "importe": "14.50", "moneda": "EUR" } }

Diseña la migración completa: ¿es rompedor?, ¿se puede evitar la v2?, ¿qué se publica y cuándo?, ¿qué cabeceras se envían y qué se comunica a cada uno de los cuatro consumidores?

Ejercicio 3: planificar la retirada de la v1

Han pasado seis meses desde que se publicó la v2 y estos son los datos de uso de la v1:

Consumidor Peticiones/mes a v1 Peticiones/mes a v2 Último acceso
SPA de la tienda web 0 4.200.000
Aroma Móvil 890.000 3.100.000 Hoy
Panel interno 12.000 45.000 Hoy
RápidoEnvíos 61.000 0 Hoy
Cliente desconocido api_key_7f2 340 0 Hace 4 meses

Decide si se puede apagar la v1 en el mes 12 y elabora el plan de acción para cada consumidor.

Soluciones

Solución 1

# Veredicto Justificación y alternativa
1 Retrocompatible Campos aditivos; los clientes que no los conocen los ignoran
2 Rompedor Todo cliente que lea notasCata recibe undefined. Alternativa: devolver los dos campos, documentar notasCata como deprecado con Sunset, y eliminarlo en la v2
3 Casi rompedor, aceptado Rompe a quien no tenga caso por defecto, pero la documentación advierte de que los enumerados crecen. Se anuncia en el changelog con antelación y se comunica a los consumidores
4 Rompedor Desaparece un dato en uso. Alternativa: dejar de devolverlo solo a los consumidores sin permiso para datos personales (04-03), que es un cambio de autorización, no de contrato; y para el resto, deprecarlo con plazo
5 Retrocompatible Ampliar los formatos aceptados en la entrada es relajar, no restringir: nadie enviaba JSON Patch antes
6 Rompedor Cambia el tipo: totalEuros * 2 deja de funcionar y las comparaciones numéricas fallan. Alternativa: añadir totalEurosTexto como campo nuevo y migrar poco a poco, o dejarlo para la v2
7 Rompedor Peticiones que funcionaban empiezan a dar 400. Alternativa: aceptarla como opcional, avisar durante meses de que será obligatoria, medir cuántos clientes la envían ya y hacerla obligatoria en la v2
8 Rompedor Peticiones válidas pasan a 400. Alternativas: optimizar la consulta; mantener 100 y limitar por consumidor mediante rate limiting (04-04); o anunciar la reducción con plazo largo y medir cuántos usan más de 50

Solución 2

¿Es rompedor? Sí, sin matices: precioEuros desaparecería y cambiaría de tipo (número → objeto). Cualquier cliente que muestre precios se rompe, y en el peor de los casos muestra [object Object].

¿Se puede evitar la v2? Sí, con la técnica de convivencia de campos, y esta es la respuesta correcta:

Fase 1 (mes 0). Se añade precio sin quitar nada:

{
  "id": "caf_001",
  "precioEuros": 14.50,
  "precio": { "importe": "14.50", "moneda": "EUR" }
}

Cambio retrocompatible: los clientes antiguos siguen leyendo precioEuros; los nuevos usan precio. Mientras solo haya euros, los dos campos coexisten sin ambigüedad. Se publica en el changelog, se documenta precioEuros como deprecado y se explica la equivalencia.

Fase 2 (meses 0-12). Respuestas con cabeceras a nivel de campo y seguimiento de uso:

Deprecation: @1773484200
Sunset: Sun, 14 Mar 2027 00:00:00 GMT
Link: <https://docs.tiendaaroma.example/migracion-precio>; rel="deprecation"

Se mide qué consumidores siguen leyendo precioEuros —lo que en la práctica exige preguntarles, porque el servidor no ve qué campos usa el cliente: aquí es donde campos= de 02-05 resulta útil como indicio.

Fase 3. Cuando aparezca la primera divisa distinta del euro, precioEuros deja de ser representable y ahí sí nace la v2, que elimina el campo antiguo. Es decir: la v2 se pospone hasta que el negocio la justifique, no la provoca un cambio de forma.

Comunicación por consumidor:

Consumidor Acción
SPA de la tienda Migración inmediata: la controlamos y se despliega en días
Aroma Móvil Migrar en la siguiente versión de la app; hay que asumir 12 meses de convivencia por las instalaciones antiguas
Panel interno Migración inmediata
RápidoEnvíos Correo formal con la guía de migración y la fecha; no consume precios, así que probablemente no le afecte, pero se le informa igual

Solución 3

Veredicto: no se puede apagar en el mes 12 sin trabajo previo. Hay dos bloqueos serios y uno menor.

Consumidor Diagnóstico Plan de acción
SPA Migrada al 100 % Nada
Aroma Móvil 890.000 llamadas/mes: hay versiones antiguas instaladas en teléfonos. No basta con publicar una versión nueva de la app Forzar la actualización desde la propia app (aviso bloqueante), medir la distribución de versiones instaladas y no apagar hasta que la curva baje del umbral acordado. Es el bloqueo principal
Panel interno 12.000 llamadas/mes: quedan pantallas sin migrar Auditoría de endpoints v1 usados y migración; es interno, así que es cuestión de planificar el trabajo. Plazo: mes 8
RápidoEnvíos 0 llamadas a v2: no ha empezado a migrar. El bloqueo más peligroso, porque es externo y no controlamos su calendario Contacto directo inmediato con su equipo técnico, guía de migración específica, entorno de pruebas y fecha comprometida por escrito. Si no puede cumplir el mes 12, se negocia una extensión acotada solo para su clave de API, con fecha nueva y firmada
api_key_7f2 340 llamadas, sin actividad en 4 meses: integración zombi o script olvidado Intentar identificar al responsable por los datos de alta; si no hay respuesta en 30 días, avisar de la retirada y apagar en la fecha prevista. No debe condicionar el plan

Plan revisado: mantener la fecha de deprecación, pero fijar el apagado en el mes 12 condicionado a dos hitos medibles: (1) que Aroma Móvil v1 baje del 2 % del tráfico total, y (2) que RápidoEnvíos confirme por escrito su migración. Entre los meses 9 y 12, recordatorios mensuales con cifras concretas; en el mes 11, v1 en solo lectura para forzar la detección de integraciones olvidadas; y un ensayo de apagado (brownout) de una hora en el mes 10, anunciado con antelación, que es la técnica más eficaz para que aparezcan los consumidores que nadie sabía que existían.

Conclusión

Versionar es caro, así que el objetivo real es necesitarlo lo menos posible. Ahora sabes distinguir con precisión un cambio retrocompatible de uno rompedor —añadir es seguro, quitar, renombrar, restringir y cambiar tipos no lo es, con la asimetría clave entre entrada y salida— y conoces la peor categoría de todas: el cambio silencioso que no falla, sino que miente. Has comparado las cinco estrategias de versionado y sabes por qué Tienda Aroma versiona en la ruta con /v1, priorizando visibilidad y coste de operación sobre la pureza REST del media type. Y tienes el ciclo de vida completo: convivencia de dos versiones como máximo sobre una lógica de negocio compartida, doce meses de plazo para los consumidores externos, cabeceras Deprecation y Sunset con Link al sucesor y a la guía de migración, métricas por consumidor para saber a quién llamar, y un apagado con 410 Gone en lugar de una redirección engañosa.

Con esto, el contrato de la v1 está completo y tiene además una política de evolución. Falta la pieza que lo convierte en algo utilizable por otros: contarlo. En la lección siguiente y última del módulo, 02-08 Documentación de APIs, veremos por qué la documentación es parte del producto, qué tipos de documento sirven a qué lector, qué debe incluir la referencia de cada endpoint, y qué cambia cuando el contrato se escribe en un formato legible por máquinas como OpenAPI. Cerraremos haciendo balance del contrato completo de Tienda Aroma y preparando el salto al módulo 3, donde por fin se implementa.

Curso de REST API: Principios de Diseño y Desarrollo de APIs RESTful

Módulo 1: Introducción a las APIs RESTful

Módulo 2: Diseño de APIs RESTful

Módulo 3: Desarrollo de APIs RESTful

Módulo 4: Buenas Prácticas y Seguridad

Módulo 5: Herramientas y Frameworks

Módulo 6: Casos de Estudio y Proyectos

© Copyright 2026. Todos los derechos reservados