Versionner une API avec Claude Code : guide pratique des contrats sûrs
Concevez un versionnage d’API avec Claude Code, OpenAPI, tests de compatibilité, en-têtes de dépréciation et rollout.
Versionner une API ne consiste pas simplement à ajouter/v2 dans une URL. C’est une promesse de compatibilité envers les applications mobiles, intégrations partenaires, services internes, consommateurs de Webhook et traitements batch qui dépendent déjà de cette API. Renommer un seul champ peut rendre la nouvelle réponse plus propre, mais casser un client ancien.
Claude Code est utile dans ce travail parce qu’il peut lire un dépôt, modifier des fichiers et exécuter des commandes, comme l’explique la documentation officielle de Claude Code. Le risque est le prompt trop vague. “Modernise cette API” pousse souvent l’assistant à optimiser la forme actuelle et à oublier les consommateurs historiques. Il faut donc lui fournir le contrat, les règles de compatibilité, le plan de rollout et les commandes de vérification avant l’implémentation.
Ce guide couvre les choix entre URL, en-tête et media type, les contrats OpenAPI, la compatibilité descendante, les en-têtesDeprecation etSunset, la politique de changelog, les consumer tests, le rollout avec fallback et les prompts Claude Code qui empêchent les breaking changes. Les références officielles utilisées sont OpenAPI Specification, RFC 9745 pour Deprecation et RFC 8594 pour Sunset.
Pour le contexte d’implémentation, consultez aussi développement d’API avec Claude Code, revue de code avec Claude Code et gestion de versions avec Changesets.
Commencer Par Le Contrat De Compatibilité
Le but du versionnage n’est pas de garder l’ancien code indéfiniment. Le but est de donner aux consommateurs une fenêtre de migration prévisible. Masa a testé ce point sur une petite API de commandes : avec un prompt limité à “ajoute v2 et renomme les champs client”, le code généré passait le nouveau tableau de bord, mais cassait un export CSV ancien. Les règles manquantes étaient concrètes : conserver la réponse v1, publier une date de dépréciation, ajouter un consumer test et documenter la migration.
Trois cas d’usage reviennent souvent :
| Cas d’usage | Contrainte principale | Style souvent adapté |
|---|---|---|
| API REST publique pour applications mobiles | D’anciennes versions restent installées plusieurs mois | Version dans l’URL |
| API partenaire B2B SaaS | Les clients migrent selon leur calendrier | URL ou en-tête explicite |
| Microservices internes | Les clients peuvent souvent être mis à jour ensemble | En-tête ou media type |
Avant de demander du code à Claude Code, notez les consommateurs actuels, la fenêtre minimale de support, la définition d’un breaking change et les métriques de suivi. Un breaking change n’est pas seulement une route supprimée. Cela peut aussi être un champ de réponse renommé, un nouveau champ requis, un format d’erreur différent, un ordre de tri par défaut modifié ou une pagination incompatible.
Choisir URL, En-Tête Ou Media Type
L’emplacement de la version affecte le routage, le cache, la documentation, la génération de SDK et le support. Pour la majorité des API publiques, la version dans l’URL est le choix pragmatique par défaut : elle apparaît dans les logs, se configure simplement dans une API Gateway et se teste facilement aveccurl. Le coût est que l’URI contient une version produit, donc/api/v1/orders/123 et/api/v2/orders/123 semblent être deux ressources différentes.
| Style | Exemple | Force | Piège fréquent |
|---|---|---|---|
| URL | /api/v1/orders | Routage, docs et debug lisibles | Les anciennes routes restent et la duplication augmente |
| En-tête | API-Version: 2 | URL stable, pratique pour clients contrôlés | En-tête oublié ; cache avecVary: API-Version |
| Media type | Accept: application/vnd.acme.orders.v2+json | Respecte la négociation de contenu HTTP | OpenAPI, SDK et support plus complexes |
Avec un media type, envoyezVary: Accept pour éviter qu’un cache intermédiaire mélange v1 et v2. Avec un en-tête personnalisé, envoyezVary: API-Version. Même avec une version dans l’URL, documentez v1 et v2 comme deux contrats OpenAPI distincts lorsque la compatibilité de réponse change.
Utiliser OpenAPI Comme Source De Vérité
OpenAPI décrit les API HTTP dans un format lisible par machine : chemins, méthodes, paramètres, corps de requête, réponses et sécurité. En clair, c’est la promesse de l’API avant l’implémentation. Le champopenapi indique la version de la spécification OpenAPI, tandis queinfo.version indique la version de votre document d’API. Précisez cette différence dans le prompt.
L’exemple suivant garde v1 documentée et marquée comme deprecated tout en ajoutant v2. Il utiliseopenapi: 3.1.0 pour une bonne compatibilité d’outillage ; vérifiez la documentation officielle OpenAPI avant d’adopter une version de spécification plus récente.
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]
Donnez d’abord ce YAML à Claude Code, puis demandez l’implémentation. L’instruction doit être explicite : ne pas supprimer les champs v1, ne pas changer les statuts v1, mettre à jour tests et changelog à chaque modification de contrat.
Implémenter La Compatibilité Dans Node
Le serveur TypeScript suivant utilise uniquement les API natives de Node. Enregistrez-le dansapi-versioning-demo.ts pour tester la version par URL, par en-têteAPI-Version et par media typeAccept, sans base de données ni framework. v1 garde l’ancienne forme, v2 expose la nouvelle, et v1 ajoute les signaux officiels de dépréciation.
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"
Le choix important est la couche de transformation. v1 ne doit pas appeler la réponse v2 en espérant que les anciens clients la tolèrent. Chaque version doit mapper le modèle interne vers la forme publique promise.
Publier Les En-Têtes Et La Politique De Version
On trouve encore des exemples avecDeprecation: true. La référence actuelle est RFC 9745 : Deprecation est une Date structurée, par exemple@1774915200. Sunset, défini par RFC 8594, est une date HTTP indiquant quand une ressource peut cesser de répondre. Ces en-têtes sont des signaux runtime, pas un plan de migration complet.
Placez la politique dans le dépôt :
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"
Le changelog doit séparer ajout, changement, dépréciation et suppression prévue. Une bonne entrée dit qui est concerné, quoi modifier, quel endpoint utiliser et quand l’ancienne version peut cesser de répondre.
Ajouter Des Consumer Tests Avant Le Refactoring
Un consumer test exprime ce dont les clients ont encore besoin. Il est précieux lorsque Claude Code refactorise une couche qui semble répétitive. Le test suivant vérifie que v1 conservecustomerName et ne retourne pas accidentellement l’objetcustomer 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 votre projet utilise un lint OpenAPI, ajoutez-le au même chemin :
npx @redocly/cli lint openapi.yaml
Ces commandes donnent à Claude Code un critère de réussite clair. “Garde la compatibilité” reste trop abstrait si vous ne dites pas quelle compatibilité est protégée.
Déployer Par Étapes Avec Fallback
Les incidents de versionnage sont souvent prévisibles. L’équipe modifie le schéma de base et la réponse dans le même déploiement, donc le rollback devient difficile. Elle annonce une date de sunset sans mesurer le trafic v1 réel. Elle met à jour le SDK mais oublie les clients HTTP directs. Ou la documentation indique deprecated alors que métriques et alertes ne montrent pas les consommateurs restants.
Le rollout doit être découpé : ajouter v2, ajouter les en-têtes de dépréciation à v1, mesurer l’usage par version, publier le guide de migration, mettre à jour les SDK, prévenir les partenaires, appliquer sunset, puis seulement supprimer v1. Le fallback doit prouver que v1 fonctionne si v2 est désactivée, que les anciens clients ignorent les nouveaux champs et que les migrations de données conservent au moins la compatibilité en lecture.
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
Joignez ces snapshots à la pull request ou donnez-les à Claude Code pour produire un résumé de compatibilité. Ils ne remplacent pas les tests, mais rendent les différences visibles.
Prompts Claude Code Contre Les Breaking Changes
Claude Code donne de meilleurs résultats quand le prompt inclut contrat, interdictions et vérifications.
Ajoute v2 à l’API existante. Traite les fichiers OpenAPI comme source de vérité. Ne change pas la forme de réponse, les codes de statut ni les en-têtes de dépréciation de v1.
Avant modification, liste :
- les breaking changes possibles
- les champs qui doivent rester en v1
- les champs ajoutés ou modifiés en v2
- les consumer tests à ajouter
Après modification, exécute :
- npm test
- npx @redocly/cli lint openapi.yaml
- comparaisons curl entre v1 et v2
Dans la réponse finale, inclus le risque de compatibilité, les notes de guide de migration et les étapes de rollback.
Avant merge, utilisez un prompt de revue :
Relis ce diff comme une revue de compatibilité API.
Vérifie :
- les champs requis de réponse v1 n’ont pas été supprimés, renommés ou changés de type
- les erreurs, statuts HTTP, paginations et tris n’ont pas changé sans intention
- Deprecation, Sunset, Link et Vary suivent la politique
- OpenAPI, implémentation, tests et CHANGELOG sont alignés
- le rollback ne casse pas les consommateurs v1
Retourne les problèmes avec fichiers et corrections concrètes.
Ces prompts déplacent l’objectif de Claude Code : non pas “rendre le code plus propre”, mais “protéger le contrat public”. Pour une API, cette différence est décisive.
Conclusion
Un versionnage sûr commence par un contrat. Choisissez URL, en-tête ou media type selon vos consommateurs et votre infrastructure. Documentez v1 et v2 dans OpenAPI, gardez des transformations explicites, publiezDeprecation etSunset, écrivez un changelog exploitable et lancez des consumer tests avant de refactoriser.
Si votre équipe veut intégrer Claude Code au développement d’API, Claude Code consultation and training peut aider à transformer contrats, gates CI, prompts de revue et checklist de rollout en processus reproductible. Pour démarrer plus petit, utilisez la cheatsheet gratuite et adaptez les prompts de cet article.
J’ai vérifié le schéma avec le serveur Node ci-dessus : v1 et v2 peuvent partager la même ligne interne tout en gardant des formes publiques différentes, et le consumer test détecte immédiatement un renommage de champ. Les détails les plus faciles à oublier sont le format Date de RFC 9745 pourDeprecation, l’en-têteVary pour les versions par en-tête ou media type, et la revue conjointe OpenAPI, code, tests et changelog.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Permission receipt Claude Code : portée, preuves et rollback
Modèle de permission receipt pour Claude Code : actions autorisées, limites d'approbation, commandes de preuve, rollback et CTAs revenus.
Agent Harness securise pour Claude Code et Codex : permissions, verification et rollback
Construisez un Agent Harness pratique pour Claude Code et Codex avec politiques, plan, verification et recuperation.
Sous-agents Claude Code : guide pratique pour déléguer sans perdre le contrôle
Guide pratique des sous-agents Claude Code pour répartir articles et code : règles, prompts, pièges et checklist.