← APIs REST avanzadas

Sesión 32 · Semana 16

Evolucionar el contrato sin romper el cliente

Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado revisar búsquedas y paginación. En Servidor continúas la implementación del mismo producto.

Se explica

25 minutos · explicación y demostración

El cliente ya depende del contrato documentado. Hoy distinguirás un cambio compatible de otro que obliga a modificarlo. Versionar ofrece una transición cuando ambos contratos deben convivir; marcar una operación como obsoleta avisa de que se retirará, sin eliminarla inmediatamente.

El coste invisible de romper un contrato publicado

Cuando desarrollas un proyecto en local, cambiar el nombre de un campo es tan fácil como pulsar Shift+F6 en IntelliJ y renombrar titulo por nombreTarea.

En producción, ese renombramiento es una bomba de relojería:

  • La aplicación móvil de los usuarios (que no se actualiza al mismo tiempo que el backend) sigue enviando y esperando titulo.
  • El serializador no encuentra el campo y asigna null.
  • Las validaciones fallan, la pantalla del cliente se congela y la tienda de aplicaciones se llena de reseñas de 1 estrella.

La ley de la inmutabilidad de contratos

Un contrato publicado en producción jamás se modifica de forma destructiva.

Las APIs evolucionan mediante adición compatible o mediante versionado explícito. Quien rompe un contrato sin aviso ni periodo de transición destruye la confianza de sus consumidores.

Cambios compatibles frente a cambios incompatibles

Antes de tocar una sola línea de código en un controlador o DTO, debes clasificar tu cambio:

Tipo de cambio Ejemplos concretos ¿Rompe a los clientes existentes?
Compatible (Non-breaking) • Añadir un nuevo endpoint a la API.
• Añadir un campo nuevo opcional en la petición de entrada.
• Añadir un campo nuevo en el JSON de respuesta.
• Relajar una restricción (ej: admitir nombres de hasta 100 caracteres en lugar de 80).
NO. Si los clientes están bien programados (lectores tolerantes), ignorarán los campos nuevos y seguirán funcionando.
Incompatible (Breaking) • Renombrar o eliminar un campo existente en el JSON.
• Cambiar el tipo de dato de un campo (ej: de número a cadena de texto).
• Hacer obligatorio un campo que antes era opcional.
• Modificar los códigos HTTP semánticos devueltos habitualmente.
• Cambiar la estructura de una respuesta (ej: transformar un array plano en un objeto paginado).
SÍ. Provoca errores inmediatos de deserialización o validación en cualquier cliente no actualizado.

Estrategias de versionado de APIs

Cuando un cambio incompatible es estrictamente necesario, la API debe ofrecer versionado:

Las tres estrategias de versionado en REST
  1. 1. Versionado en URI (/api/v1/proyectos)
  2. 2. Versionado por Header (Accept / Custom)
  3. 3. Versionado por Query Param (?version=1)
  • 1 · Versionado en la URI (Recomendado en la industria): GET /api/v1/proyectos frente a GET /api/v2/proyectos.
    • Ventajas: Totalmente explícito, fácil de probar en el navegador y almacenable en cachés HTTP intermedias sin problemas.
  • 2 · Versionado por Cabecera (Content Negotiation): Accept: application/vnd.empresa.v1+json.
    • Ventajas: Mantiene la URI limpia y puramente orientada al recurso; sin embargo, dificulta las pruebas manuales y complica la configuración de proxies.
  • 3 · Versionado por Parámetro: GET /proyectos?v=2.
    • Ventajas: Sencillo de añadir; sin embargo, no suele considerarse una buena práctica arquitectónica para cambios estructurales de recursos.

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

Paso 1 · Retomar el proyecto y preparar la comprobación

  1. Guarda una colección que represente al cliente actual y abre las rutas que vas a evolucionar. No la edites todavía para adaptarla al cambio.
  2. Elige un campo añadido y un cambio incompatible, como renombrar uno requerido por el cliente. Escribe el efecto previsto de cada uno.
  3. Localiza las rutas que ya consume Intermodular y define cómo seguirán funcionando mientras se actualiza el consumidor.

Paso 2 · La Ley de Postel (Principio de Robustez)

«Sé conservador con lo que envías, y liberal con lo que aceptas.» — Jon Postel

Aplicado a APIs REST modernas:

  1. Al recibir datos ( liberal ): El backend debe ignorar propiedades desconocidas que envíe el cliente en lugar de rechazar la petición con error 400. En Spring Boot esto es el comportamiento por defecto de Jackson (FAIL_ON_UNKNOWN_PROPERTIES = false).
  2. Al enviar datos ( conservador ): El backend debe enviar únicamente los campos acordados en el contrato, sin alterar sus nombres ni sus tipos de datos.

Paso 3 · El protocolo de obsolescencia (Deprecation y Sunset Headers)

Cuando una versión o endpoint va a desaparecer, no se apaga sin previo aviso. Se aplica un periodo de gracia informando a los clientes a través de cabeceras HTTP estándar (RFC 8594):

HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: true
Sunset: Wed, 11 Nov 2026 00:00:00 GMT
Link: </api/v2/proyectos>; rel="successor-version"
  • Deprecation: true: Advierte a las herramientas de monitorización de que el endpoint está obsoleto.
  • Sunset: Declara la fecha y hora exacta a partir de la cual el endpoint dejará de existir y devolverá 410 Gone o 404 Not Found.

Paso 4 · Versionar rutas y añadir campos de forma compatible

Guarda una copia exportada de la colección que representa al cliente anterior. En el controlador identifica un cambio compatible, como un campo opcional añadido, y otro incompatible, como una clave renombrada. Mantén temporalmente las rutas anteriores mientras conectas /api/v1; registra la transición en la documentación y prueba ambas colecciones. No cambies a la vez todas las URLs del cliente antiguo: dejarías de comprobar su compatibilidad.

Modifica únicamente el @RequestMapping de tu ProyectoController existente, conservando el cuerpo completo:

@RequestMapping({"/proyectos", "/api/v1/proyectos"})

Así ambas rutas llegan temporalmente a la misma implementación. No crees un controlador vacío ni dupliques los endpoints. Actualiza Location y la colección para utilizar la nueva ruta, manteniendo una prueba del cliente anterior.

Supongamos que el equipo de producto nos pide que los proyectos incluyan una etiqueta de color corporativo opcional:

// Evolución compatible: el nuevo campo tiene valor por defecto si no viene
public record ProyectoResponse(
    Long id,
    String nombre,
    String descripcion,
    boolean activo,
    String colorHex, // Campo nuevo añadido sin eliminar ninguno anterior
    LocalDateTime creadoEn
) {}

Un cliente antiguo que solo lea id y nombre seguirá funcionando al 100 %, mientras que los nuevos clientes podrán hacer uso del nuevo campo colorHex.

Si un método antiguo va a ser reemplazado, inyectamos las cabeceras estándar en el ResponseEntity:

@Deprecated(since = "1.5.0", forRemoval = true)
@Operation(summary = "Endpoint legado de detalle", deprecated = true)
@GetMapping("/legado/{id}")
public ResponseEntity<ProyectoResponse> obtenerLegado(@PathVariable Long id) {
    ProyectoResponse dto = proyectoService.buscarPorId(id);

    return ResponseEntity.ok()
        .header("Deprecation", "true")
        .header("Sunset", "Fri, 01 Jan 2027 00:00:00 GMT")
        .header("Link", "</api/v1/proyectos/" + id + ">; rel=\"successor-version\"")
        .body(dto);
}

Paso 5 · Simular clientes antiguos en Bruno

  1. Petición del cliente tolerante: Ejecuta POST /api/v1/proyectos enviando un campo adicional desconocido en el JSON:
    {
      "nombre": "Proyecto Beta",
      "descripcion": "Prueba",
      "campoExtraClienteAntiguo": "valor-ignorado"
    }
    • Comprueba que Spring Boot responde con código 201 Created sin fallar, demostrando el cumplimiento de la Ley de Postel.
  2. Petición al endpoint legado: Ejecuta GET /api/v1/proyectos/legado/1.
    • Comprueba en la pestaña de Headers de Bruno que la respuesta contiene Deprecation: true y la fecha de expiración en Sunset.

Paso 6 · Migrar tu API entera a /api/v1

Haz inventario de todos los prefijos actuales, añade /api/v1 de manera consistente y actualiza la variable base de la colección si incluyes allí ese prefijo. Comprueba que no aparece dos veces, como /api/v1/api/v1. Ajusta también Location, rutas anidadas, pruebas y configuración CORS. Retira las rutas anteriores solo cuando hayas migrado los consumidores acordados; documenta cualquier retirada en vez de presentarla como un cambio compatible.

  1. Añade el prefijo /api/v1 al @RequestMapping de todos tus controladores. No lo pongas endpoint a endpoint: un solo sitio por controlador.
  2. Actualiza tu colección de peticiones. Si has usado una variable de entorno para la URL base —como enseñaba la sesión 7—, este paso es un único cambio; si escribiste la URL a mano en cada petición, hoy descubres por qué aquello importaba.
  3. Ejecuta la suite de tests. Los de @WebMvcTest van a fallar en bloque porque las rutas han cambiado: eso es exactamente lo que deben hacer. Corrígelos y observa que la suite acaba de avisarte de un cambio que rompe el contrato, que es para lo que existe.
  4. Comprueba que la cabecera Location de los 201 Created también lleva el prefijo. Si la construyes con ServletUriComponentsBuilder.fromCurrentRequest(), se actualiza sola; si la escribiste a mano, ahora apunta a una ruta que ya no existe.
  5. Regenera la documentación OpenAPI de la sesión 31 y comprueba que Swagger refleja las rutas nuevas.
  6. Aplica una evolución compatible sobre TareaResponse: añade el campo diasActiva, calculado a partir de la fecha de creación, sin tocar ningún campo existente. Vuelve a ejecutar los tests y comprueba que siguen en verde: añadir un campo no rompe a nadie, y esa asimetría —añadir es seguro, quitar y renombrar no— es la regla que hay que memorizar de esta sesión.
  7. Escribe en tu cuaderno los tres cambios que romperían a un consumidor: quitar un campo, renombrarlo y cambiar su tipo. Junto a cada uno, cómo se hace de forma segura con /v2 y las cabeceras Deprecation y Sunset.
Cómo saber que lo has terminado
No queda ninguna ruta sin el prefijo /api/v1; la colección entera vuelve a pasar; los tests están corregidos y en verde; la cabecera Location apunta a una URL que existe; y Swagger muestra las rutas nuevas.

Paso 7 · Comprobar y registrar el resultado del proyecto

  1. Ejecuta la colección del cliente anterior contra las rutas mantenidas y verifica que sigue funcionando durante la transición.
  2. Prueba también el contrato nuevo y comprueba los avisos de obsolescencia previstos. Actualiza documentación y consumidor antes de retirar una ruta antigua.

Ampliación si has completado el trabajo

Primero termina y verifica los pasos anteriores. Estos retos profundizan en el mismo contenido; no sustituyen la entrega ni obligan a iniciar otro proyecto.

Reto · Matriz de compatibilidad y contratos automatizados

Cuando múltiples servicios independientes colaboran en producción, la compatibilidad no puede dejarse a la memoria de los programadores.

Investiga el concepto de pruebas de contrato dirigidas por el consumidor (Consumer-Driven Contract Testing) con herramientas como Pact:

  1. ¿Cómo permite un test de contrato asegurar que un cambio en el backend no romperá a la aplicación móvil antes de desplegar en producción?
  2. ¿Por qué las pruebas de contrato son infinitamente más rápidas y estables que desplegar todos los servicios juntos en un entorno de pruebas End-to-End (E2E)?

Formato de entrega

Incluye esta explicación en el registro de la sesión dentro del repositorio de GitHub, junto al código y las comprobaciones. La entrega es el enlace al repositorio y al commit de la sesión.

Objetivo mínimoClasificación de cambios compatibles e incompatibles comprendida y prefijo /api/v1 configurado.
Si lo tienesEvolución compatible de DTOs aplicada con tests en verde y cabeceras Deprecation y Sunset configuradas.
RetoPropuesta técnica de Consumer-Driven Contracts documentada y justificada para entornos distribuidos.
Ver respuestas

1 · Porque cualquier cliente previamente desplegado que busque el nombre antiguo recibirá null o sufrirá un error de deserialización, provocando fallos en su interfaz o lógica.

2 · «Sé conservador con lo que envías y liberal con lo que aceptas». En Spring Boot implica no rechazar peticiones que incluyan campos adicionales desconocidos (FAIL_ON_UNKNOWN_PROPERTIES=false).

3 · Es explícito, directamente legible y fácil de probar en navegadores y herramientas, y compatible de forma nativa con todas las capas de infraestructura y caché HTTP intermedias.

4 · La fecha y hora exacta (en formato HTTP-date estándar) a partir de la cual el endpoint dejará de estar disponible y será definitivamente eliminado del servidor.

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

El consumidor sigue funcionando durante la transición y las versiones compatibles quedan registradas.

Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.