Versionado de API con Claude Code: guía práctica para contratos seguros
Diseña versionado de API con Claude Code usando OpenAPI, pruebas de compatibilidad, cabeceras de deprecación y rollout.
Versionar una API no consiste solo en añadir/v2 a una ruta. Es una promesa de compatibilidad para cada aplicación móvil, integración de partner, servicio interno, consumidor de Webhook y proceso batch que ya depende de esa API. Si cambias el nombre de un campo sin una ruta de migración, el endpoint nuevo puede parecer más limpio, pero un cliente antiguo puede romperse.
Claude Code acelera este trabajo porque puede leer el repositorio, editar archivos y ejecutar comandos, como describe la documentación oficial de Claude Code. El riesgo aparece cuando el prompt es vago: “moderniza esta API” suele llevar al asistente a optimizar la forma nueva y a olvidar consumidores antiguos. La solución es entregar contrato, reglas de compatibilidad, plan de despliegue y comandos de verificación antes de tocar código.
Esta guía cubre las decisiones entre URL, cabecera y media type, contratos OpenAPI, compatibilidad hacia atrás, cabecerasDeprecation ySunset, política de changelog, consumer tests, rollout con fallback y prompts para que Claude Code no introduzca breaking changes. Las referencias oficiales son OpenAPI Specification, RFC 9745 para Deprecation y RFC 8594 para Sunset.
Para completar el flujo, lee también desarrollo de API con Claude Code, code review con Claude Code y gestión de versiones con Changesets.
Empieza Por El Contrato De Compatibilidad
El objetivo del versionado no es mantener código viejo para siempre. Es permitir que los consumidores migren en una ventana conocida. Masa lo probó con una API pequeña de pedidos: cuando el prompt decía solo “añade v2 y renombra los campos de cliente”, el código generado pasaba el dashboard nuevo, pero rompía una exportación CSV antigua. Faltaban instrucciones concretas: conservar la forma de respuesta v1, publicar la fecha de deprecación, añadir consumer tests y documentar la migración.
Tres casos de uso aparecen con frecuencia:
| Caso de uso | Restricción clave | Estilo que suele encajar |
|---|---|---|
| API REST pública para apps móviles | Versiones antiguas de la app siguen instaladas durante meses | Versión en la URL |
| API B2B para partners SaaS | Cada cliente migra con su propio calendario | URL o cabecera explícita |
| Microservicios internos | Los clientes suelen poder actualizarse juntos | Cabecera o media type |
Antes de pedir implementación a Claude Code, documenta consumidores actuales, ventana mínima de soporte, definición de breaking change y métricas que se observarán. Un breaking change no es solo borrar una ruta. También puede ser renombrar un campo de respuesta, añadir un campo requerido, cambiar el formato de error, alterar el orden por defecto o modificar la paginación.
Elige URL, Cabecera O Media Type
La ubicación de la versión afecta al enrutamiento, caché, documentación, generación de SDK y soporte. Para la mayoría de APIs públicas, la versión en la URL es el punto de partida más pragmático: aparece en logs, es simple para API Gateway y resulta fácil de probar concurl. El coste es que el URI del recurso contiene una versión de producto, así que/api/v1/orders/123 y/api/v2/orders/123 parecen recursos distintos.
| Estilo | Ejemplo | Ventaja | Fallo común |
|---|---|---|---|
| URL | /api/v1/orders | Rutas, docs y depuración claras | Rutas antiguas permanecen y crece la duplicación |
| Cabecera | API-Version: 2 | URL estable y clientes controlados | La cabecera se olvida; la caché necesitaVary: API-Version |
| Media type | Accept: application/vnd.acme.orders.v2+json | Encaja con negociación de contenido HTTP | OpenAPI, SDKs y soporte se vuelven más complejos |
Si usas media type, envíaVary: Accept para que una caché intermedia no mezcle respuestas v1 y v2. Si usas una cabecera personalizada, envíaVary: API-Version. Incluso con versionado en URL, trata v1 y v2 como contratos separados en OpenAPI cuando cambia la compatibilidad de respuesta.
Usa OpenAPI Como Fuente De Verdad
OpenAPI describe APIs HTTP de forma legible por máquinas: paths, métodos, parámetros, cuerpos, respuestas y seguridad. En términos simples, es la promesa de la API antes de implementarla. El campoopenapi indica la versión de la especificación OpenAPI, mientras queinfo.version indica la versión del documento de tu API. Díselo explícitamente a Claude Code para evitar confusiones.
El ejemplo mantiene v1 documentada y marcada como deprecated mientras añade v2. Usaopenapi: 3.1.0 por compatibilidad con validadores y generadores; revisa el sitio oficial de OpenAPI si tu equipo quiere adoptar una versión de especificación más reciente.
openapi: 3.1.0
info:
title: Acme Orders API
version: 2.0.0
servers:
- url: https://api.example.com
paths:
/api/v1/orders/{orderId}:
get:
operationId: getOrderV1
summary: Get an order in the legacy v1 shape
deprecated: true
x-deprecated-at: "2026-03-31T00:00:00Z"
x-sunset-at: "2026-12-31T23:59:59Z"
parameters:
- name: orderId
in: path
required: true
schema:
type: string
responses:
"200":
description: Legacy order response
headers:
Deprecation:
schema:
type: string
description: RFC 9745 structured date, for example @1774915200
Sunset:
schema:
type: string
description: RFC 8594 HTTP-date when v1 may stop responding
content:
application/json:
schema:
$ref: "#/components/schemas/OrderV1Envelope"
/api/v2/orders/{orderId}:
get:
operationId: getOrderV2
summary: Get an order in the current v2 shape
parameters:
- name: orderId
in: path
required: true
schema:
type: string
responses:
"200":
description: Current order response
content:
application/json:
schema:
$ref: "#/components/schemas/OrderV2Envelope"
components:
schemas:
OrderV1Envelope:
type: object
required: [data]
properties:
data:
type: object
required: [id, customerName, totalCents, currency]
properties:
id:
type: string
customerName:
type: string
totalCents:
type: integer
currency:
type: string
OrderV2Envelope:
type: object
required: [data]
properties:
data:
type: object
required: [id, customer, amount, status]
properties:
id:
type: string
customer:
type: object
required: [displayName]
properties:
displayName:
type: string
amount:
type: object
required: [value, currency]
properties:
value:
type: integer
currency:
type: string
status:
type: string
enum: [paid, shipped]
Primero entrega este YAML a Claude Code y después pide implementación. La instrucción debe decir: no eliminar campos v1, no cambiar códigos de estado v1 y actualizar pruebas y changelog cuando cambie el contrato.
Implementa Compatibilidad Hacia Atrás En Node
El siguiente servidor TypeScript usa solo APIs nativas de Node. Puedes copiarlo comoapi-versioning-demo.ts y probar versionado por URL, por cabeceraAPI-Version y por media typeAccept sin base de datos ni framework. v1 conserva la forma antigua, v2 devuelve la forma actual y v1 incluye señales oficiales de deprecación.
import { createServer } from "node:http";
import { parse } from "node:url";
type ApiVersion = "v1" | "v2";
type OrderRow = {
id: string;
customerName: string;
totalCents: number;
currency: "JPY" | "USD";
status: "paid" | "shipped";
createdAt: string;
};
const orders = new Map<string, OrderRow>([
[
"o_100",
{
id: "o_100",
customerName: "Masa Tanaka",
totalCents: 129800,
currency: "JPY",
status: "paid",
createdAt: "2026-06-02T09:00:00.000Z",
},
],
]);
function detectVersion(req: { headers: Record<string, string | string[] | undefined> }, pathname: string) {
const pathVersion = pathname.match(/^\/api\/(v[12])\//)?.[1] as ApiVersion | undefined;
if (pathVersion) return { version: pathVersion, source: "path" };
const header = req.headers["api-version"];
if (typeof header === "string") {
const normalized = header.startsWith("v") ? header : `v${header}`;
if (normalized === "v1" || normalized === "v2") {
return { version: normalized, source: "header" };
}
throw new Error(`Unsupported API-Version: ${header}`);
}
const accept = req.headers.accept;
if (typeof accept === "string") {
const mediaMatch = accept.match(/application\/vnd\.acme\.orders\.v([12])\+json/);
if (mediaMatch) {
return { version: `v${mediaMatch[1]}` as ApiVersion, source: "media-type" };
}
}
return { version: "v1" as ApiVersion, source: "default" };
}
function orderIdFrom(pathname: string) {
return pathname.match(/^\/api\/(?:v[12]\/)?orders\/([^/]+)$/)?.[1];
}
function toV1(row: OrderRow) {
return {
data: {
id: row.id,
customerName: row.customerName,
totalCents: row.totalCents,
currency: row.currency,
},
};
}
function toV2(row: OrderRow) {
return {
data: {
id: row.id,
customer: { displayName: row.customerName },
amount: { value: row.totalCents, currency: row.currency },
status: row.status,
createdAt: row.createdAt,
},
};
}
function addDeprecationHeaders(res: import("node:http").ServerResponse) {
const deprecatedAt = Math.floor(Date.parse("2026-03-31T00:00:00Z") / 1000);
res.setHeader("Deprecation", `@${deprecatedAt}`);
res.setHeader("Sunset", new Date("2026-12-31T23:59:59Z").toUTCString());
res.setHeader(
"Link",
[
'<https://docs.example.com/api/deprecations/v1-to-v2>; rel="deprecation"; type="text/html"',
'<https://docs.example.com/api/sunset-policy>; rel="sunset"; type="text/html"',
].join(", "),
);
}
function sendJson(res: import("node:http").ServerResponse, status: number, body: unknown) {
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
res.end(JSON.stringify(body, null, 2));
}
const server = createServer((req, res) => {
const pathname = parse(req.url ?? "/").pathname ?? "/";
const orderId = orderIdFrom(pathname);
if (!orderId) {
return sendJson(res, 404, { error: "not_found", message: "Route not found" });
}
let detected: ReturnType<typeof detectVersion>;
try {
detected = detectVersion(req, pathname);
} catch (error) {
return sendJson(res, 400, {
error: "unsupported_version",
message: error instanceof Error ? error.message : "Unsupported API version",
supportedVersions: ["v1", "v2"],
});
}
const row = orders.get(orderId);
if (!row) {
return sendJson(res, 404, { error: "order_not_found", orderId });
}
res.setHeader("Vary", "Accept, API-Version");
res.setHeader("X-API-Version", detected.version);
res.setHeader("X-API-Version-Source", detected.source);
if (detected.version === "v1") {
addDeprecationHeaders(res);
return sendJson(res, 200, toV1(row));
}
return sendJson(res, 200, toV2(row));
});
const port = Number(process.env.PORT ?? 18080);
server.listen(port, () => {
console.log(`API versioning demo: http://localhost:${port}`);
});
npm init -y
npm install -D tsx typescript @types/node
PORT=18080 npx tsx api-versioning-demo.ts &
SERVER_PID=$!
sleep 1
curl -i http://localhost:18080/api/v1/orders/o_100
curl -i -H "API-Version: 2" http://localhost:18080/api/orders/o_100
curl -i -H "Accept: application/vnd.acme.orders.v2+json" http://localhost:18080/api/orders/o_100
kill "$SERVER_PID"
La decisión importante es la capa de transformación. v1 no debe llamar a la respuesta v2 y confiar en que los clientes antiguos la toleren. Cada versión se mapea desde el modelo interno hasta la forma pública prometida.
Publica Cabeceras De Deprecación Y Política
Muchos ejemplos antiguos usanDeprecation: true. Hoy conviene seguir RFC 9745: Deprecation es un valor Date estructurado, por ejemplo@1774915200. Sunset, definido por RFC 8594, es una fecha HTTP que indica cuándo un recurso puede dejar de responder. Estas cabeceras son señales de runtime, no un plan completo de migración.
Guarda la política en el repositorio para que Claude Code, reviewers y equipos de negocio lean las mismas reglas.
currentApiVersion: v2
minimumSupportWindowMonths: 12
breakingChangeRequires:
- new-major-version
- migration-guide
- consumer-test
- owner-approval
deprecatedVersions:
- version: v1
deprecatedAt: "2026-03-31T00:00:00Z"
sunsetAt: "2026-12-31T23:59:59Z"
replacement: "/api/v2/orders/{orderId}"
migrationGuide: "https://docs.example.com/api/deprecations/v1-to-v2"
El changelog debe separar añadido, cambiado, deprecated y eliminación prevista. Una entrada útil dice quién se ve afectado, qué debe cambiar, qué endpoint reemplaza al anterior y cuándo la versión antigua puede dejar de responder.
Añade Consumer Tests Antes De Refactorizar
Un consumer test expresa lo que el cliente todavía necesita. Es especialmente útil cuando Claude Code refactoriza una capa que parece duplicada. La prueba siguiente confirma que v1 mantienecustomerName y no devuelve por accidente el objetocustomer de v2.
import assert from "node:assert/strict";
import test from "node:test";
const baseUrl = process.env.API_BASE_URL ?? "http://localhost:18080";
test("v1 keeps the legacy response shape", async () => {
const res = await fetch(`${baseUrl}/api/v1/orders/o_100`);
assert.equal(res.status, 200);
assert.match(res.headers.get("deprecation") ?? "", /^@\d+$/);
assert.match(res.headers.get("sunset") ?? "", /GMT$/);
const body = await res.json();
assert.equal(body.data.customerName, "Masa Tanaka");
assert.equal(body.data.customer, undefined);
});
test("v2 returns the current response shape", async () => {
const res = await fetch(`${baseUrl}/api/orders/o_100`, {
headers: { "API-Version": "2" },
});
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.data.customer.displayName, "Masa Tanaka");
assert.equal(body.data.amount.currency, "JPY");
assert.equal(body.data.customerName, undefined);
});
PORT=18080 npx tsx api-versioning-demo.ts &
SERVER_PID=$!
sleep 1
API_BASE_URL=http://localhost:18080 node --test version-contract.test.mjs
kill "$SERVER_PID"
Si el proyecto usa lint de OpenAPI, inclúyelo en el mismo camino:
npx @redocly/cli lint openapi.yaml
Cuando estos comandos están en el prompt, Claude Code tiene un criterio concreto. Si solo dices “mantén compatibilidad”, todavía tiene que adivinar qué significa compatibilidad en tu producto.
Despliega Por Fases Y Prepara Fallback
Los incidentes de versionado suelen repetirse. El equipo cambia esquema de base de datos y respuesta en el mismo deploy, de modo que el rollback es difícil. Se anuncia una fecha de sunset sin medir tráfico real de v1. Se actualiza el SDK, pero se olvida a quienes llaman HTTP directamente. O la documentación dice deprecated, mientras métricas y alertas no identifican consumidores restantes.
El rollout debe separarse: añadir v2, añadir cabeceras de deprecación a v1, medir uso por versión, publicar guía de migración, actualizar SDKs, notificar partners, aplicar sunset y solo después eliminar v1. El fallback debe probar que v1 sigue funcionando si se desactiva v2, que los clientes antiguos ignoran campos nuevos y que la migración de datos mantiene lectura compatible.
mkdir -p tmp/version-snapshots
BASE_URL=${BASE_URL:-http://localhost:18080}
for order_id in o_100 missing; do
curl -sS -D "tmp/version-snapshots/${order_id}.v1.headers" \
"$BASE_URL/api/v1/orders/$order_id" \
> "tmp/version-snapshots/${order_id}.v1.json" || true
curl -sS -D "tmp/version-snapshots/${order_id}.v2.headers" \
-H "API-Version: 2" \
"$BASE_URL/api/orders/$order_id" \
> "tmp/version-snapshots/${order_id}.v2.json" || true
done
Adjunta estos snapshots al pull request o pégalos en Claude Code para pedir un resumen de compatibilidad. No reemplazan pruebas, pero hacen visibles las diferencias de comportamiento.
Prompts Para Evitar Breaking Changes
Claude Code funciona mejor cuando el prompt incluye contrato, cambios prohibidos y comprobaciones obligatorias.
Añade v2 a la API existente. Trata los archivos OpenAPI como fuente de verdad. No cambies la forma de respuesta, códigos de estado ni cabeceras de deprecación de v1.
Antes de editar, enumera:
- posibles breaking changes
- campos que deben permanecer en v1
- campos añadidos o modificados en v2
- consumer tests que vas a añadir
Después de editar, ejecuta:
- npm test
- npx @redocly/cli lint openapi.yaml
- comparaciones curl para v1 y v2
En la respuesta final, incluye riesgo de compatibilidad, notas para la guía de migración y pasos de rollback.
Antes de fusionar, usa un prompt de revisión:
Revisa este diff como revisión de compatibilidad de API.
Comprueba:
- los campos requeridos de respuesta v1 no fueron eliminados, renombrados ni cambiados de tipo
- errores, estados HTTP, paginación y orden no cambiaron inesperadamente
- Deprecation, Sunset, Link y Vary coinciden con la política
- OpenAPI, implementación, pruebas y CHANGELOG están alineados
- el rollback no rompe consumidores v1
Devuelve hallazgos con archivos y arreglos concretos.
Estos prompts cambian el objetivo de Claude Code: de “limpiar el código” a “proteger el contrato público”. En APIs, esa diferencia pesa más que la sintaxis exacta de la versión.
Conclusión
El versionado seguro empieza con un contrato. Elige URL, cabecera o media type según consumidores e infraestructura. Documenta v1 y v2 en OpenAPI, mantén transformaciones explícitas, publicaDeprecation ySunset, escribe un changelog accionable y ejecuta consumer tests antes de refactorizar.
Si tu equipo quiere introducir Claude Code en el desarrollo de APIs, Claude Code consultation and training puede ayudar a convertir contratos, gates de CI, prompts de revisión y checklist de rollout en un flujo repetible. Para empezar en pequeño, usa la cheatsheet gratuita y adapta los prompts de este artículo.
Verifiqué el patrón con el servidor Node anterior: v1 y v2 pueden compartir la misma fila interna y mantener formas públicas distintas, y el consumer test detecta un renombrado de campo de inmediato. Los detalles más fáciles de olvidar fueron el formato Date de RFC 9745 paraDeprecation, la cabeceraVary en versionado por cabecera o media type, y revisar OpenAPI, código, pruebas y changelog juntos.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Permission receipt para Claude Code: alcance, prueba y rollback
Patrón de permission receipt para Claude Code: acciones permitidas, aprobación, pruebas, rollback y CTA de ingresos.
Agent Harness seguro para Claude Code y Codex: permisos, verificacion y rollback
Diseña un Agent Harness seguro para Claude Code y Codex con permisos, plan, verificaciones y rollback.
Subagentes de Claude Code: guía práctica para delegar trabajo de forma segura
Guía práctica de subagentes en Claude Code para dividir artículos y código: reglas, prompts, riesgos y checklist.