StackPractices
intermediate Por Mathias Paulenko

Versionado de APIs: URL, Header y Media Type

Cómo versionar APIs REST con estrategias de ruta URL, header y media-type manteniendo compatibilidad hacia atrás para los clientes existentes.

Temas: api

Visión General

El versionado de APIs mantiene a los clientes existentes funcionando mientras la API evoluciona. Cuando se renombra un campo, cambia la forma de una respuesta o se endurecen los requisitos de auth, los clientes del contrato antiguo se rompen a menos que las versiones convivan. Esta receta implementa los tres enfoques dominantes — ruta URL, header personalizado y negociación de contenido (media type) — como middleware en Flask, Express y Spring Boot, además de la mecánica de deprecación que permite retirar versiones de forma segura. Para la comparación completa de estrategias, consulta la Guía Completa de Estrategias de Versionado de APIs.

Cuándo Usar

Usa este recurso cuando:

  • Publiques una API pública consumida por clientes externos que no puedes actualizar simultáneamente
  • Introduzcas cambios que rompen compatibilidad como campos eliminados, recursos renombrados o nuevos requisitos de auth
  • Soportes varias generaciones de clientes: apps móviles, integraciones de terceros, widgets embebidos
  • Planifiques una migración gradual desde una forma legada de API a un diseño moderno. Consulta Llamar a una API REST para patrones del lado cliente

Omite el versionado cuando controlas tanto la API como su único consumidor: despliega los cambios que rompen compatibilidad de forma atómica en lugar de cargar con la indirección de versiones.

Solución

Python (Flask + Versionado por Ruta URL)

from flask import Flask, jsonify, request

app = Flask(__name__)

# Registro de blueprints versionados
from v1 import users as users_v1
from v2 import users as users_v2

app.register_blueprint(users_v1.bp, url_prefix="/api/v1/users")
app.register_blueprint(users_v2.bp, url_prefix="/api/v2/users")

# Alternativa basada en header
@app.before_request
def version_from_header():
    version = request.headers.get("X-API-Version", "v1")
    request.api_version = version

@app.route("/api/users/<int:id>")
def get_user(id):
    if request.api_version == "v2":
        return jsonify({"id": id, "full_name": "Alice", "email": "alice@example.com"})
    return jsonify({"id": id, "name": "Alice"})

# Negociación de contenido (header Accept)
@app.route("/api/users")
def list_users():
    accept = request.headers.get("Accept", "")
    if "application/vnd.myapp.v2+json" in accept:
        return jsonify({"users": [{"full_name": "Alice"}]})
    return jsonify({"users": [{"name": "Alice"}]})

JavaScript (Express + Versionado por Ruta URL)

import express from "express";

const app = express();

// Versionado a nivel de ruta
app.use("/api/v1/users", (await import("./routes/v1/users.js")).default);
app.use("/api/v2/users", (await import("./routes/v2/users.js")).default);

// Versionado por header vía middleware
function apiVersion(req, res, next) {
  req.apiVersion = req.headers["x-api-version"] || "v1";
  next();
}

app.get("/api/users/:id", apiVersion, (req, res) => {
  if (req.apiVersion === "v2") {
    return res.json({ id: req.params.id, full_name: "Alice", email: "alice@example.com" });
  }
  res.json({ id: req.params.id, name: "Alice" });
});

// Negociación de contenido
app.get("/api/users", (req, res) => {
  const accept = req.get("Accept") || "";
  if (accept.includes("application/vnd.myapp.v2+json")) {
    return res.json({ users: [{ full_name: "Alice" }] });
  }
  res.json({ users: [{ name: "Alice" }] });
});

// Variante por query parameter — válida para prototipos, más débil para caché
app.get("/api/items", (req, res) => {
  const version = req.query.version === "v2" ? "v2" : "v1";
  res.json({ version, items: [] });
});

Java (Spring Boot + Ruta URL y Header)

import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;

@RestController
public class UserController {

  // Versionado por ruta URL
  @GetMapping("/api/v1/users/{id}")
  public UserV1 getUserV1(@PathVariable Long id) {
    return new UserV1(id, "Alice");
  }

  @GetMapping("/api/v2/users/{id}")
  public UserV2 getUserV2(@PathVariable Long id) {
    return new UserV2(id, "Alice", "alice@example.com");
  }

  // Versionado por header
  @GetMapping(value = "/api/users/{id}", headers = "X-API-Version=v1")
  public UserV1 getUserHeaderV1(@PathVariable Long id) {
    return getUserV1(id);
  }

  @GetMapping(value = "/api/users/{id}", headers = "X-API-Version=v2")
  public UserV2 getUserHeaderV2(@PathVariable Long id) {
    return getUserV2(id);
  }

  // Negociación de contenido (produces)
  @GetMapping(value = "/api/users/{id}", produces = "application/vnd.myapp.v2+json")
  public UserV2 getUserMediaV2(@PathVariable Long id) {
    return getUserV2(id);
  }
}

record UserV1(Long id, String name) {}
record UserV2(Long id, String fullName, String email) {}

Explicación

  • Versionado por ruta URL (/v1/, /v2/) es el enfoque más simple y el más amigable con la caché. Es visible, fácil de documentar y funciona con cualquier cliente HTTP. La contrapartida: ensucia el espacio de URLs y obliga a los clientes a cambiarlas en cada actualización que rompe compatibilidad.
  • Versionado por header (X-API-Version: v2) mantiene las URLs limpias pero requiere headers personalizados, que los navegadores y los scripts simples no siempre manejan bien. Cachear a nivel CDN exige reglas explícitas.
  • Negociación de contenido (Accept: application/vnd.myapp.v2+json) es el enfoque más RESTful. Usa la maquinaria estándar de HTTP pero resulta más difícil para los consumidores y se mezcla mal con las expectativas de application/json plano.
  • Compatibilidad hacia atrás significa solo cambios aditivos dentro de una versión: nuevos campos opcionales, nuevos endpoints y enums ampliados son seguros. Eliminar o renombrar campos exige una versión nueva.
  • Deprecación funciona mejor cuando es legible por máquina: devuelve los headers Deprecation y Sunset (consulta la Plantilla de Aviso de Deprecación de API) para que los clientes detecten los plazos sin leer documentación.
  • La caché se comporta distinto según la estrategia: el versionado por ruta da a cada versión su propia clave de caché, mientras que el versionado por header o media type necesita un header Vary que fragmenta las entradas de caché por cliente.

Variantes

EstrategiaMecanismoIdeal para
Ruta URL/api/v1/resourceAPIs públicas, caché simple, soporte amplio de clientes
Header personalizadoX-API-Version: v2APIs internas, URLs limpias, routing consciente del CDN
Media TypeAccept: application/vnd.app.v2+jsonDiseño RESTful estricto, APIs orientadas a contenido
Query parameter?version=v2Prototipado, implementación de cliente más simple
Mermaid flowchart LR diagram

Mejores Prácticas

  1. Empieza con versionado por ruta URL: es el más descubrible y no requiere lógica especial del cliente.
  2. Nunca rompas una versión publicada: una vez en producción, mantenla hasta la fecha de retirada anunciada.
  3. Versiona solo ante cambios que rompen compatibilidad: los cambios aditivos (nuevos campos opcionales, nuevos endpoints) no necesitan una versión nueva.
  4. Limita las versiones activas: cada versión extra multiplica routing, tests, documentación y superficie de despliegue. Dos versiones activas más una en retirada es un techo razonable para la mayoría de equipos.
  5. Versiona el esquema de respuesta, no solo las rutas: los payloads pueden divergir incluso bajo la misma URL.
  6. Usa versiones semánticas internamente y solo major en público: v1.3.2 para paquetes y seguimiento interno, /v1/ para la superficie pública.
  7. Publica una guía de migración por versión mayor: ejemplos lado a lado del comportamiento antiguo y el nuevo reducen los tickets de soporte durante la transición.
  8. Señala la deprecación en headers: Deprecation: true y Sunset: <fecha-HTTP> dan a los clientes programáticos visibilidad de los plazos sin leer docs.

Errores Comunes

  1. Subir la versión por cada cambio menor, fragmentando el ecosistema de clientes. Consulta Validación de Entrada para cambios aditivos que mantienen compatibilidad.
  2. Eliminar versiones antiguas sin un período de retirada, rompiendo integraciones de producción de la noche a la mañana.
  3. Mezclar estrategias de versionado entre endpoints de la misma API: versiona toda la superficie, no endpoints individuales.
  4. No normalizar los identificadores de versión, de modo que v1.0 y v1 enrutan accidentalmente a handlers distintos.
  5. Devolver formas de error o códigos de estado distintos entre versiones sin documentarlo.
  6. Mantener tres o más versiones vivas indefinidamente: cada una arrastra sus propios controladores, serializadores, tests y monitorización.

Solución de Problemas

  • 404 en una ruta versionada: la versión no está registrada en el gateway o el router. Confirma que la tabla de rutas cubre todas las versiones activas.
  • Clientes que siguen llamando a una versión retirada: verifica que los headers Deprecation y Sunset les llegan de verdad y revisa el tráfico por versión antes de imponer 410 Gone.
  • Respuestas obsoletas tras publicar una versión nueva: el CDN cacheó el payload anterior. El versionado por header o media type necesita un header Vary y claves de caché distintas; purga las entradas afectadas.
  • v1.0 y v1 tratados como versiones distintas: normaliza el identificador de versión en la entrada y rechaza valores fuera del conjunto soportado.
  • Clientes de navegador que ignoran el header de versión: los navegadores no siempre pueden adjuntar headers personalizados en peticiones cross-origin. Sirve una alternativa por ruta o query param en los endpoints orientados a navegador.

Estrategia de Testing

  • Enrutado de versiones: llama a los endpoints /v1/ y /v2/ y confirma que llegan a los handlers correctos.
  • Headers de deprecación: llama a una versión deprecada y verifica que la respuesta lleva los headers Deprecation, Sunset y Link.
  • Compatibilidad hacia atrás: ejecuta la suite de tests de v1 contra v2; todos los tests de v1 deberían pasar salvo que el cambio fuera intencional.
  • Comportamiento en la retirada: simula la fecha de sunset y confirma que la API devuelve 410 Gone con un apuntador a la versión vigente.
  • Detección de cambios que rompen compatibilidad: corre oasdiff u openapi-diff en CI para marcar cambios de contrato entre versiones antes del despliegue.

Monitorización

  • Reparto de tráfico por versión: cuando v2 supera a v1, planea la retirada de v1. Cuando una versión queda por debajo del ~1% del tráfico durante 30 días, es candidata a desmantelarse.
  • Impresiones de headers de deprecación: cuenta cuántos clientes siguen recibiendo Deprecation y Sunset; ese es tu backlog de migración.
  • 404 en versiones desconocidas: peticiones a /v3/ cuando solo existen v1-v2 indican clientes mal configurados. Registra la versión pedida y el ID de cliente para poder avisarles.
  • Latencia por versión: las versiones nuevas deberían ser igual o más rápidas; si v2 retrocede, mira los campos y queries nuevos antes de culpar al routing.

Cuándo No Usar Este Enfoque

  • APIs internas con un solo consumidor: si controlas ambos lados, despliega los cambios que rompen compatibilidad de forma atómica. El versionado añade indirección que no necesitas.
  • APIs de prototipo y MVP: el versionado formal frena la iteración mientras el contrato sigue cambiando cada semana.
  • APIs GraphQL: GraphQL evoluciona mediante cambios aditivos de esquema y campos @deprecated, no mediante versiones de URL.

Consideraciones de Seguridad

  • Valida los headers de versión: con versionado por header, acepta solo valores conocidos y rechaza el resto: un X-API-Version sin validar es un vector de inyección y de confusión de rutas.
  • Mantén la forma de los errores idéntica entre versiones: payloads de error divergentes permiten a un atacante identificar qué versión del stack responde.

Ver También

Preguntas frecuentes

¿Cuándo debería lanzar una nueva versión de API?

Solo ante cambios que rompen compatibilidad: campos eliminados, recursos renombrados, requisitos de auth modificados o comportamiento alterado del que dependen clientes existentes. Los cambios aditivos (campos opcionales nuevos, endpoints nuevos) no requieren una versión nueva.

¿Debería usar versionado por ruta URL o por header?

Usa ruta URL (/v1/) para APIs públicas: es visible, trivialmente cacheable y funciona en navegadores, curl y proxies. El versionado por header o media type encaja en APIs internas donde controlas los clientes y valoras las URLs limpias.

¿Puedo soportar varias versiones con la misma base de código?

Sí. Usa controladores o handlers de ruta versionados que deleguen en servicios compartidos. Mantén la lógica específica de versión delgada (serialización, validación) y la lógica de negocio agnóstica de versión. Spring Boot, Express y Flask soportan este patrón limpiamente.

¿Cómo funcionan los headers Deprecation y Sunset?

Deprecation marca un endpoint como deprecado y Sunset: <fecha-HTTP> anuncia cuándo dejará de funcionar, de modo que los clientes detectan los plazos programáticamente en lugar de leer changelogs. Consulta la Plantilla de Aviso de Deprecación de API para la parte de comunicación.

¿Cuánto tiempo debería mantener viva una versión antigua de API?

Para APIs públicas: 12-24 meses con avisos de deprecación activos. Para APIs internas: 3-6 meses o hasta que todos los clientes conocidos migren. Siempre monitoriza el tráfico hacia las versiones antiguas y contacta a los usuarios activos antes de imponer la retirada.