Advanced (Actualizado: 2/6/2026)

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.

Versionado de API con Claude Code: guía práctica para contratos seguros

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 usoRestricción claveEstilo que suele encajar
API REST pública para apps móvilesVersiones antiguas de la app siguen instaladas durante mesesVersión en la URL
API B2B para partners SaaSCada cliente migra con su propio calendarioURL o cabecera explícita
Microservicios internosLos clientes suelen poder actualizarse juntosCabecera 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.

EstiloEjemploVentajaFallo común
URL/api/v1/ordersRutas, docs y depuración clarasRutas antiguas permanecen y crece la duplicación
CabeceraAPI-Version: 2URL estable y clientes controladosLa cabecera se olvida; la caché necesitaVary: API-Version
Media typeAccept: application/vnd.acme.orders.v2+jsonEncaja con negociación de contenido HTTPOpenAPI, 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.

#Claude Code #diseño de API #versionado de API #OpenAPI #TypeScript
Gratis

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.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.