Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 044 — Versionado de API#

⬅️ 043 · 📚 Parte 3 · 🎓 Clases · 045 ➡️ Parte 3 — Validación y contrato · Nivel 🔴 avanzado · Pista backendClase construida — 4 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Servir dos versiones vivas del mismo recurso sin romper a quien ya te consume. Y comparar las dos formas de hacerlo.

🧩 La situación#

La v1 devuelve nombre: "Ada Lovelace". La v2 lo separa en nombre y apellido. Es el cambio incompatible más común que existe, y las dos versiones conviven.

📖 Las dos formas#

En la ruta#

GET /v1/personas/1
GET /v2/personas/1

A favor: se ve en el registro, en la caché y en el navegador. Se prueba con curl sin pensar. Un cliente antiguo no puede acabar en la v2 por accidente.

En contra: el mismo recurso tiene dos URL. Quien defiende REST con rigor objeta que la identidad del recurso no debería cambiar porque cambie su representación.

En la cabecera#

GET /personas/1
X-Api-Version: 2

A favor: una sola URL por recurso, que es lo que el modelo de REST propone.

En contra: invisible. No se ve en el registro sin configurarlo, la caché necesita Vary —la trampa de la clase 018— y probar con el navegador es incómodo.

Y un detalle que decide más de lo que parece: si el cliente no envía la cabecera, ¿qué versión recibe? Estas cuatro implementaciones sirven la v1, que es la respuesta correcta: no romper a quien no pidió nada.

🧮 El contrato#

Petición Respuesta
GET /v1/personas/1 {"id":"1","nombre":"Ada Lovelace"}
GET /v2/personas/1 {"id":"1","nombre":"Ada","apellido":"Lovelace"}
GET /personas/1 sin cabecera la v1
con x-api-version: 2 la v2
igual la respuesta declara x-api-version: 2
con x-api-version: 9 400 · VERSION_DESCONOCIDA

Los dos últimos casos merecen atención. Declarar qué versión sirvió permite diagnosticar: sin eso, un cliente que recibe la forma equivocada no sabe si el problema es suyo o del servidor. Y rechazar una versión desconocida en lugar de adivinar evita que un error tipográfico se sirva en silencio como v1.

<!-- generado: fichas -->

📖 Las palabras que esta clase define#

Si alguna de estas no te dice nada todavía, esta es la clase donde se aprende. Las definiciones viven en el glosario, que reúne las del programa entero.

Palabra Qué significa
Versionado de API Cómo se publica un cambio que rompe a los clientes existentes. En la ruta, en una cabecera o por negociación de contenido; lo que no cambia es la obligación de decidir cuánto tiempo se mantiene la versión anterior.

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
Express framework web de Node.js (JavaScript) 2010 MIT OpenJS Foundation
FastAPI framework web de Python (Python) 2018 MIT proyecto independiente
Spring Boot framework de aplicación de JVM (Java) 2014 Apache-2.0 Broadcom/VMware y colaboradores
ASP.NET Core framework web de .NET (C#) 2016 MIT Microsoft y .NET Foundation

🔧 Express#

Definió el modelo de middleware encadenado que copiaron casi todos los frameworks de Node.js. Minimalista no significa biblioteca: posee el bucle de peticiones.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 FastAPI#

Deriva validación, serialización y documentación OpenAPI de las anotaciones de tipo. Demostró que el tipado opcional de Python podía ser infraestructura, no adorno.

Arrancarla suelta, sin el verificador:

PORT=3000 python -m uvicorn main:app --host 127.0.0.1 --port 3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
main.py código Python
requirements.txt dependencias de Python, una por línea, con versión fijada

🔧 Spring Boot#

Autoconfiguración y servidor incrustado sobre Spring. Convirtió un framework famoso por su configuración XML en uno de arranque inmediato.

Preparar sus dependencias, dentro de su directorio:

mvn -q -B package -DskipTests

Arrancarla suelta, sin el verificador:

PORT=3000 java -jar target/clase-044-1.0.0.jar --server.port=3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
pom.xml manifiesto de Maven: el proyecto, su Java, sus dependencias y cómo se empaqueta
src/main/java/labs/Aplicacion.java código Java

🔧 ASP.NET Core#

Reescritura multiplataforma y de código abierto de la pila web de Microsoft. Sus API mínimas trajeron el estilo de los microframeworks al ecosistema .NET.

Preparar sus dependencias, dentro de su directorio:

dotnet build -c Release --nologo -v quiet

Arrancarla suelta, sin el verificador:

PORT=3000 dotnet run -c Release --no-build --urls http://127.0.0.1:3000

Qué hay dentro de su directorio:

Archivo Qué es
Clase044.csproj proyecto de .NET: el marco de destino y las dependencias
Program.cs código C#
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Los cuatro sirven el mismo recurso en dos representaciones y lo hacen por dos vías a la vez: la versión en la ruta y la versión en una cabecera. Lo que hay que mirar es cómo se separa el código de cada versión, porque de eso depende que la v1 pueda congelarse mientras la v2 evoluciona.

El cambio incompatible del ejemplo es el más común que existe:

const PERSONA = { id: "1", nombre: "Ada", apellido: "Lovelace" };

La v1 tenía un solo campo nombre; la v2 lo partió en dos. Cualquier cliente que leyera nombre esperando el nombre completo se rompe.

Express · express/server.mjs#

app.get("/v1/personas/1", (peticion, respuesta) => {
  respuesta.json({ id: PERSONA.id, nombre: `${PERSONA.nombre} ${PERSONA.apellido}` });
});

app.get("/v2/personas/1", (peticion, respuesta) => {
  respuesta.json(PERSONA);
});

Dos rutas, dos manejadores, cero acoplamiento. Tocar la v2 no puede romper la v1 porque no comparten una línea.

Y la segunda vía, en la misma aplicación:

app.get("/personas/1", (peticion, respuesta) => {
  const version = peticion.get("x-api-version") ?? "1";
  respuesta.set("x-api-version", version);

  if (version === "2") return respuesta.json(PERSONA);

Aquí sí hay un if. El argumento de quien defiende la cabecera está en el comentario del propio archivo: la identidad del recurso no cambia porque cambie su representación, así que la URL debería ser una sola.

Fíjate en que la versión se devuelve en la respuesta. Sin esa cabecera, un cliente que pide la 2 y recibe la 1 —porque el servidor todavía no la soporta— no tiene forma de saberlo.

FastAPI · fastapi/main.py — un enrutador por versión#

v1 = APIRouter(prefix="/v1")
v2 = APIRouter(prefix="/v2")
app.include_router(v1)
app.include_router(v2)

Un enrutador por versión, no dos rutas sueltas. La diferencia importa cuando la API tiene cuarenta rutas: el prefijo se declara una vez, y congelar la v1 es dejar de tocar un objeto entero.

def persona(
    x_api_version: Annotated[str, Header()] = "1",
) -> JSONResponse:

Y la cabecera llega por la firma, con su valor por omisión declarado — la misma virtud de la clase 016.

ASP.NET Core · aspnet-core/Program.cs — un grupo por versión#

var v1 = app.MapGroup("/v1");
var v2 = app.MapGroup("/v2");

v1.MapGet("/personas/1", () => Results.Json(ComoV1()));
v2.MapGet("/personas/1", () => Results.Json(persona));

MapGroup es el equivalente exacto del APIRouter de FastAPI, y tiene una ventaja que aparece pronto: al grupo se le pueden colgar filtros, autorización y metadatos que se aplican a todas sus rutas. Congelar la v1 puede llegar a ser literal — un filtro que rechace escrituras en todo el grupo.

    return version switch
    {
        "2" => Results.Json(persona),
        "1" => Results.Json(ComoV1()),
        _ => Results.Json(new { code = "VERSION_DESCONOCIDA" }, statusCode: 400),
    };

Y el switch de expresión obliga a cubrir el caso por omisión, así que la versión desconocida no se puede olvidar.

Spring Boot · spring-boot/…/Aplicacion.java — enruta por la cabecera#

    @GetMapping(value = "/personas/1", headers = "X-Api-Version=2")
    public ResponseEntity<Map<String, String>> porCabeceraV2() {
        return ResponseEntity.ok().header("X-Api-Version", "2").body(PERSONA);
    }

Esto es lo más interesante de la clase. headers = "X-Api-Version=2" hace que Spring enrute por la cabecera: son dos métodos distintos para la misma ruta y el despachador elige cuál llamar.

Es más limpio que un if dentro de un método compartido, y por un motivo concreto: cada versión es un método independiente que se puede congelar, probar y borrar por separado. Retirar la v1 es borrar un método; en Express y FastAPI es editar una condición dentro de un manejador que la v2 también usa.

    public ResponseEntity<Map<String, String>> porCabecera(
            @RequestHeader(name = "X-Api-Version", required = false, defaultValue = "1")
            String version) {

El método sin headers queda como el caso por omisión: recoge todo lo que no declare la versión 2, y decide entre servir la 1 o rechazar.

Lo que los cuatro persiguen#

Un if (version == 2) dentro de un manejador compartido consigue lo contrario de lo que se busca: las dos versiones acopladas en el mismo código, donde tocar una arriesga la otra y retirar la vieja es cirugía.

Las tres formas que se ven aquí —prefijo de ruta, enrutador o grupo, y enrutado por cabecera— tienen en común que el código de cada versión vive aparte. Esa es la propiedad, no la sintaxis.

🔬 Comparación#

Framework Por ruta Por cabecera Separación del código
Spring Boot anotación enruta por cabecera un método por versión
FastAPI enrutador con prefijo con un if enrutador por versión
ASP.NET Core grupo de rutas con un switch grupo por versión
Express montaje de enrutador con un if manual

Spring es el único que enruta por la cabecera en lugar de ramificar dentro del manejador. La diferencia se nota cuando hay tres versiones vivas: tres métodos independientes frente a un switch de sesenta líneas.

🧭 La pregunta que va antes#

¿Necesitas versionar?

Versionar duplica el código que mantienes, las pruebas que ejecutas y la documentación que publicas. La clase 050 muestra que la mayoría de los cambios se pueden hacer compatibles: añadir campos opcionales, devolver el campo viejo y el nuevo a la vez, ampliar en lugar de estrechar.

El orden sensato:

  1. Intenta que el cambio sea compatible. Casi siempre se puede.
  2. Si no, versiona — y planifica desde el principio cuándo retiras la antigua, porque una versión sin fecha de retirada es una versión eterna.
  3. Nunca cambies el significado sin cambiar la versión.

Geewax es tajante en esto: el coste real del versionado no es publicarlo, es mantenerlo durante años [geewax-api-design-patterns].

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 044

🧪 Reto de transferencia#

Añade Vary: X-Api-Version a las respuestas de la ruta con cabecera y explica, con el caso de la clase 018, qué le pasaría a un cliente si no estuviera. Después decide cuál de las dos formas usarías en una API pública y justifícalo.

🔗 Enlaces#

Fuentes#