Advanced (Diperbarui: 2/6/2026)

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 dengan Claude Code: panduan praktis kontrak yang aman

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 caseBatasan utamaGaya yang biasanya cocok
Public REST API untuk mobile appVersi app lama tetap terpasang berbulan-bulanURL path versioning
B2B SaaS partner APICustomer migrasi sesuai jadwal sendiriURL path atau header eksplisit
Microservices internalClient sering bisa di-upgrade bersamaHeader 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.

GayaContohKekuatanKegagalan umum
URL path/api/v1/ordersRouting, docs, dan debug jelasPath lama bertahan dan router menjadi duplikatif
Custom headerAPI-Version: 2URL stabil, cocok untuk client terkontrolHeader mudah lupa; cache perluVary: API-Version
Media typeAccept: application/vnd.acme.orders.v2+jsonSelaras dengan HTTP content negotiationOpenAPI, 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.

#Claude Code #desain API #API versioning #OpenAPI #TypeScript
Gratis

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.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.