API Versioning dengan Claude Code: panduan praktis kontrak yang aman
Rancang API versioning aman dengan Claude Code, OpenAPI, compatibility test, header deprecation, dan rollout.
API versioning bukan sekadar menambahkan/v2 ke route. Ini adalah janji kompatibilitas untuk mobile app, integrasi partner, service internal, consumer Webhook, dan batch job yang sudah bergantung pada API tersebut. Satu nama field yang berubah bisa membuat respons baru terlihat lebih rapi, tetapi mematahkan client lama.
Claude Code membantu karena dapat membaca codebase, mengedit file, dan menjalankan command, seperti dijelaskan di dokumentasi resmi Claude Code. Risikonya muncul ketika prompt terlalu umum. Permintaan seperti “modernisasi API ini” sering membuat assistant fokus pada bentuk baru dan melupakan consumer lama. Agar aman, berikan contract, aturan kompatibilitas, rencana rollout, dan command verifikasi sebelum implementasi.
Panduan ini membahas tradeoff URL, header, dan media type versioning, kontrak OpenAPI, backward compatibility, headerDeprecation danSunset, kebijakan changelog, consumer test, rollout dengan fallback, serta prompt Claude Code yang mencegah breaking change. Referensi resmi yang digunakan adalah OpenAPI Specification, RFC 9745 untuk Deprecation header, dan RFC 8594 untuk Sunset header.
Untuk konteks lanjutan, baca juga pengembangan API dengan Claude Code, code review dengan Claude Code, dan manajemen versi dengan Changesets.
Mulai Dari Kontrak Kompatibilitas
Tujuan versioning bukan mempertahankan kode lama selamanya. Tujuannya adalah memberi consumer waktu migrasi yang jelas. Masa menguji pola ini pada API pesanan kecil: ketika prompt hanya berkata “tambahkan v2 dan rename field customer”, kode yang dihasilkan lolos dashboard baru, tetapi merusak export CSV lama. Yang kurang bukan kemampuan coding, melainkan aturan: bentuk respons v1 harus tetap, tanggal deprecation harus dipublikasikan, consumer test harus ditambah, dan migration guide harus diperbarui.
Tiga use case yang sering muncul:
| Use case | Batasan utama | Gaya yang biasanya cocok |
|---|---|---|
| Public REST API untuk mobile app | Versi app lama tetap terpasang berbulan-bulan | URL path versioning |
| B2B SaaS partner API | Customer migrasi sesuai jadwal sendiri | URL path atau header eksplisit |
| Microservices internal | Client sering bisa di-upgrade bersama | Header atau media type |
Sebelum meminta Claude Code menulis implementasi, tulis consumer saat ini, minimum support window, definisi breaking change, dan metrik yang akan dipantau. Breaking change bukan hanya route yang dihapus. Field respons yang di-rename, request field baru yang wajib, envelope error yang berubah, default sort yang berubah, atau pagination baru juga bisa mematahkan client lama.
Pilih URL, Header, Atau Media Type
Lokasi versi memengaruhi routing, cache, dokumentasi, SDK generation, dan support. Untuk sebagian besar public API, URL path versioning adalah default yang pragmatis: terlihat di log, sederhana di API Gateway, dan mudah diuji dengancurl. Kekurangannya adalah URI resource ikut membawa versi produk, sehingga/api/v1/orders/123dan/api/v2/orders/123terlihat seperti resource berbeda.
| Gaya | Contoh | Kekuatan | Kegagalan umum |
|---|---|---|---|
| URL path | /api/v1/orders | Routing, docs, dan debug jelas | Path lama bertahan dan router menjadi duplikatif |
| Custom header | API-Version: 2 | URL stabil, cocok untuk client terkontrol | Header mudah lupa; cache perluVary: API-Version |
| Media type | Accept: application/vnd.acme.orders.v2+json | Selaras dengan HTTP content negotiation | OpenAPI, SDK, dan support jadi lebih kompleks |
Jika memakai media type, kirimVary: Accept agar cache perantara tidak mencampur respons v1 dan v2. Jika memakai custom header, kirimVary: API-Version. Bahkan dengan URL versioning, perlakukan v1 dan v2 sebagai kontrak OpenAPI terpisah saat kompatibilitas respons berubah.
Jadikan OpenAPI Source Of Truth
OpenAPI mendeskripsikan HTTP API dalam format yang bisa dibaca mesin: path, method, parameter, request body, response, dan security. Sederhananya, ini adalah janji API sebelum implementasi. Fieldopenapi berarti versi spesifikasi OpenAPI, sedangkaninfo.version berarti versi dokumen API milik tim. Jelaskan perbedaan ini ke Claude Code.
Contoh berikut mempertahankan dokumentasi v1 dan menandainya sebagai deprecated sambil menambahkan v2. Ia memakaiopenapi: 3.1.0 karena banyak validator dan generator mendukungnya dengan baik; cek dokumentasi resmi OpenAPI sebelum memilih versi spesifikasi yang lebih baru.
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]
Berikan YAML ini ke Claude Code sebelum meminta implementasi. Instruksinya harus jelas: jangan hapus field v1, jangan ubah status code v1, dan update test serta CHANGELOG setiap kali kontrak berubah.
Implementasikan Backward Compatibility Di Node
Server TypeScript berikut hanya memakai API bawaan Node. Simpan sebagaiapi-versioning-demo.ts untuk menguji versioning via URL, headerAPI-Version, dan media typeAccept tanpa database atau framework. v1 mempertahankan respons legacy, v2 mengembalikan respons saat ini, dan v1 mengirim header deprecation.
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"
Keputusan terpenting adalah lapisan transformasi. v1 tidak boleh memakai respons v2 lalu berharap client lama bisa menerima. Setiap versi memetakan model internal ke bentuk publik yang dijanjikan kontrak.
Publikasikan Header Deprecation Dan Kebijakan Versi
Banyak contoh lama memakaiDeprecation: true. Sesuai RFC 9745,Deprecation adalah nilai Date terstruktur, misalnya@1774915200. Sunset menurut RFC 8594 adalah HTTP-date yang memberi tahu kapan resource mungkin berhenti merespons. Header ini adalah sinyal runtime, bukan pengganti migration plan.
Simpan kebijakan di repository:
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"
CHANGELOG harus memisahkan added, changed, deprecated, dan planned removal. Entri yang baik menyebut siapa yang terdampak, apa yang harus diubah, endpoint pengganti, dan kapan versi lama mungkin berhenti merespons.
Tambahkan Consumer Test Sebelum Refactor
Consumer test menyatakan apa yang masih diharapkan client. Ini penting ketika Claude Code ingin membersihkan transformasi yang terlihat duplikatif. Test berikut memastikan v1 tetap punyacustomerName dan tidak tanpa sengaja mengembalikan objekcustomer milik 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"
Jika proyek memakai OpenAPI lint, tambahkan ke jalur verifikasi yang sama:
npx @redocly/cli lint openapi.yaml
Dengan command ini di prompt, Claude Code punya target konkret. Kalimat “jaga kompatibilitas” saja masih terlalu luas.
Rollout Bertahap Dan Siapkan Fallback
Insiden API versioning biasanya mudah diprediksi. Tim mengubah schema database dan bentuk respons dalam deploy yang sama, sehingga rollback sulit. Tanggal sunset diumumkan sebelum traffic v1 diukur. SDK diperbarui, tetapi pengguna HTTP langsung terlupakan. Atau dokumentasi menyebut deprecated, sementara metrik dan alert tidak menunjukkan consumer yang tersisa.
Pisahkan rollout: tambahkan v2, tambahkan header deprecation ke v1, ukur penggunaan per versi, publikasikan migration guide, update SDK, beri tahu partner, terapkan sunset, lalu baru hapus v1. Fallback harus membuktikan v1 tetap berjalan saat v2 dimatikan, client lama mengabaikan field baru, dan migrasi data minimal tetap read-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
Lampirkan snapshot ini ke pull request atau berikan ke Claude Code untuk membuat ringkasan kompatibilitas. Ini tidak menggantikan test, tetapi membuat perbedaan perilaku terlihat.
Prompt Claude Code Untuk Mencegah Breaking Change
Claude Code bekerja lebih baik jika prompt berisi contract, perubahan yang dilarang, dan check wajib.
Tambahkan v2 ke API yang ada. Perlakukan file OpenAPI sebagai source of truth. Jangan ubah bentuk respons v1, status code, atau header deprecation.
Sebelum edit, daftar:
- kemungkinan breaking change
- field yang harus tetap ada di v1
- field yang ditambah atau diubah di v2
- consumer test yang akan ditambahkan
Setelah edit, jalankan:
- npm test
- npx @redocly/cli lint openapi.yaml
- perbandingan curl untuk v1 dan v2
Di jawaban akhir, sertakan risiko kompatibilitas, catatan migration guide, dan langkah rollback.
Sebelum merge, pakai prompt review:
Review diff ini sebagai API compatibility review.
Periksa:
- required response field v1 tidak dihapus, di-rename, atau berubah tipe
- error envelope, HTTP status, pagination, dan sort order tidak berubah tak sengaja
- Deprecation, Sunset, Link, dan Vary sesuai kebijakan
- OpenAPI, implementasi, test, dan CHANGELOG selaras
- rollback tidak mematahkan consumer v1
Kembalikan temuan dengan nama file dan perbaikan konkret.
Prompt ini menggeser tujuan Claude Code dari “membuat kode lebih bersih” menjadi “melindungi kontrak publik”. Dalam API, perbedaan itu lebih penting daripada lokasi angka versi.
Kesimpulan
API versioning yang aman dimulai dari kontrak. Pilih URL, header, atau media type berdasarkan consumer dan infrastruktur. Dokumentasikan v1 dan v2 di OpenAPI, pertahankan transformasi eksplisit, publikasikanDeprecationdanSunset, tulis CHANGELOG yang bisa ditindaklanjuti, dan jalankan consumer test sebelum refactor.
Jika tim Anda ingin memasukkan Claude Code ke workflow pengembangan API, Claude Code consultation and training bisa membantu mengubah kontrak API, CI gate, prompt review, dan checklist rollout menjadi proses berulang. Untuk mulai kecil, gunakan free cheatsheet dan adaptasikan prompt dari artikel ini.
Saya memverifikasi pola ini dengan server Node di atas: v1 dan v2 bisa berbagi row internal yang sama sambil menjaga bentuk publik berbeda, dan consumer test langsung menangkap rename field. Detail yang paling mudah terlewat adalah format Date RFC 9745 untukDeprecation, headerVary pada header/media-type versioning, dan review OpenAPI, kode, test, serta CHANGELOG secara bersamaan.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Permission receipt Claude Code: mencatat scope, bukti, dan rollback
Pola permission receipt untuk Claude Code: aksi yang diizinkan, batas approval, command verifikasi, rollback, dan cek CTA revenue.
Agent Harness Aman untuk Claude Code dan Codex: Permission, Verifikasi, dan Rollback
Rancang Agent Harness praktis untuk Claude Code dan Codex dengan policy, plan, verification, dan recovery layer.
Subagent Claude Code: panduan praktis untuk delegasi artikel dan kode
Panduan subagent Claude Code untuk membagi pekerjaan artikel dan kode: aturan delegasi, prompt, risiko, dan checklist.