Versioning kontrak API tanpa merusak integrasi klien lama berarti satu hal: perubahan yang berpotensi breaking tidak boleh diam-diam mengganti perilaku lama. Jika klien lama masih mengirim payload, membaca enum, memverifikasi signature webhook, atau mengandalkan format error tertentu, server harus tetap menghormati kontrak yang mereka pilih sampai masa transisinya selesai.

Pendekatan yang paling aman adalah menganggap versi kompatibilitas sebagai mode eksplisit, mirip ide editions: perubahan besar dibungkus sebagai kontrak baru yang dipilih secara sadar oleh klien melalui path, header, atau mekanisme negosiasi yang jelas. Dengan begitu, Anda bisa mengembangkan API tanpa membuat klien lama rusak hanya karena default server berubah.

Apa yang Sebenarnya Perlu Di-version?

Banyak tim terlalu cepat membuat /v2, tetapi tidak mendefinisikan apa yang sedang di-version. Yang perlu dijaga bukan hanya URL, melainkan kontrak: struktur request/response, tipe data, field wajib, enum, kode error, perilaku autentikasi, aturan retry, urutan event webhook, dan kadang semantik bisnis.

Beberapa perubahan tampak kecil tetapi sebenarnya breaking:

  • Mengubah field dari string menjadi object.
  • Menghapus field yang sebelumnya selalu ada.
  • Mengubah enum dari paid menjadi succeeded.
  • Mengubah aturan auth dari API key di header menjadi token bertanda tangan tanpa masa transisi.
  • Mengubah arti nilai null atau field kosong.
  • Mengubah format timestamp dari Unix epoch ke ISO-8601.

Karena itu, diskusi versi API harus dimulai dari pertanyaan: apakah perubahan ini mengubah kontrak yang diandalkan klien?

Additive Change vs Breaking Change

Perubahan additive

Perubahan additive umumnya aman jika klien ditulis defensif. Contohnya:

  • Menambah field baru opsional pada response.
  • Menambah endpoint baru.
  • Menambah parameter request opsional dengan default yang tidak mengubah perilaku lama.
  • Menambah nilai enum hanya jika dokumentasi sejak awal mewajibkan klien menangani nilai tak dikenal dengan aman.

Contoh response sebelum dan sesudah perubahan additive:

// sebelum
{
  "id": "inv_123",
  "status": "paid",
  "amount": 150000
}

// sesudah (additive)
{
  "id": "inv_123",
  "status": "paid",
  "amount": 150000,
  "paid_at": "2025-01-10T08:15:00Z"
}

Ini relatif aman jika klien mengabaikan field yang tidak dikenali. Jika ada klien yang melakukan validasi ketat dan menolak field tambahan, bahkan perubahan additive bisa memicu kegagalan. Itu sebabnya kontrak perlu mendefinisikan toleransi parser dengan jelas.

Perubahan breaking

Perubahan breaking adalah perubahan yang dapat membuat klien lama gagal parse, gagal autentikasi, atau salah mengambil keputusan bisnis.

// sebelum
{
  "id": "inv_123",
  "status": "paid"
}

// sesudah (breaking)
{
  "id": "inv_123",
  "payment": {
    "status": "succeeded"
  }
}

Perubahan di atas breaking karena:

  • status dipindahkan ke lokasi baru.
  • Nilai enum berubah dari paid ke succeeded.
  • Klien lama yang membaca status di root akan gagal atau mengambil keputusan yang salah.

Aturan praktisnya:

  • Additive: tambahkan, jangan ubah arti yang lama.
  • Breaking: jika arti, struktur, kewajiban, atau perilaku berubah, buat versi kompatibilitas baru.

Pola Versioning: Path, Header, atau Per Endpoint?

1. Versi pada path

GET /v1/invoices/inv_123
GET /v2/invoices/inv_123

Kelebihan: mudah dipahami, mudah dicatat di log, mudah di-debug dengan curl, dan jelas di gateway atau reverse proxy.

Kekurangan: sering mendorong duplikasi endpoint besar-besaran walau perubahan hanya menyentuh sebagian kontrak. Selain itu, tim cenderung mencampur versi URL dengan semantik resource yang sebenarnya sama.

2. Versi pada header

GET /invoices/inv_123
API-Version: 2025-01-01

atau:

Accept: application/vnd.example+json; version=2025-01-01

Kelebihan: resource tetap stabil, versi menjadi atribut kontrak, dan cocok untuk gagasan edition: perilaku dipilih secara eksplisit oleh klien.

Kekurangan: lebih sulit dilihat sekilas, rawan terlewat di tooling, dan perlu disiplin pada dokumentasi, observabilitas, serta cache key.

3. Versi per endpoint atau per capability

Pendekatan ini berguna jika hanya sebagian area API yang berubah besar. Misalnya endpoint pembayaran membutuhkan kontrak baru, sedangkan endpoint katalog tetap sama.

Kelebihan: migrasi lebih terarah, tidak memaksa semua area ikut naik versi.

Kekurangan: kompleksitas dokumentasi naik karena sistem bisa memiliki beberapa “zona kompatibilitas”.

Rekomendasi praktis

Jika Anda ingin meniru semangat editions, versi berbasis header atau atribut kontrak biasanya paling cocok. Klien secara eksplisit memilih kontrak yang diinginkan, misalnya:

API-Version: 2024-10-01

Server lalu menerapkan:

  • shape response sesuai versi tersebut,
  • enum sesuai versi tersebut,
  • aturan auth dan error mapping sesuai versi tersebut bila perlu,
  • serialisasi webhook sesuai versi endpoint pendaftarannya.

Yang penting bukan medianya, melainkan prinsipnya: jangan ubah default lama secara diam-diam untuk klien yang tidak meminta perubahan itu.

Merancang “Compatibility Edition” untuk Kontrak API

Pola ini berguna saat perubahan besar perlu dikirim tanpa merusak klien lama. Alih-alih mengganti perilaku global API, buat lapisan resolusi kontrak berdasarkan versi yang dipilih.

Konsep dasarnya

  1. Setiap request memiliki resolved contract version.
  2. Versi itu menentukan parser request, validator, serializer response, format error, dan kadang policy auth.
  3. Default untuk klien lama tidak berubah.
  4. Klien baru memilih versi baru secara eksplisit.

Contoh alur resolusi

function resolveContractVersion(request) {
  if (request.header['API-Version']) return request.header['API-Version']
  if (request.apiKey has pinnedVersion) return request.apiKey.pinnedVersion
  return '2024-10-01' // default lama yang stabil
}

Pendekatan ini sering lebih aman dibanding menaikkan default untuk semua klien. Bahkan jika Anda ingin mendorong adopsi versi baru, lakukan lewat provisioning klien baru atau lewat opt-in, bukan dengan mengubah perilaku klien lama yang masih aktif.

Version pinning per kredensial

Salah satu praktik yang sangat membantu adalah menyimpan versi kontrak pada API key, OAuth client, atau account integration. Dengan begitu:

  • request tanpa header versi tetap konsisten,
  • tim support bisa melihat klien berada di kontrak mana,
  • migrasi dapat dilakukan per integrasi, bukan serentak.

Perubahan Skema, Field, dan Enum yang Aman

Field baru

Field baru sebaiknya:

  • bersifat opsional,
  • tidak mengubah arti field lama,
  • didokumentasikan sebagai tambahan, bukan pengganti diam-diam.

Field yang akan diganti

Jika ingin mengganti field lama dengan bentuk yang lebih baik, jangan langsung menghapus field lama. Sediakan masa overlap.

// kontrak lama
{
  "customer_name": "Budi"
}

// fase transisi
{
  "customer_name": "Budi",
  "customer": {
    "name": "Budi"
  }
}

Setelah itu, field baru bisa diwajibkan hanya pada versi kontrak baru.

Enum

Enum sering menjadi sumber bug integrasi pihak ketiga karena parser klien biasanya menganggap nilai enum tertutup. Praktik yang lebih aman:

  • hindari mengganti nama enum pada versi yang sama,
  • anggap penambahan enum sebagai perubahan yang perlu komunikasi jelas,
  • dokumentasikan bahwa klien harus punya fallback untuk nilai tak dikenal,
  • jika arti status berubah, buat versi baru.

Contoh transisi enum yang aman:

// v1
status: "paid" | "pending" | "failed"

// v2
payment_status: "succeeded" | "processing" | "failed" | "requires_action"

Jangan ubah enum lama menjadi enum baru di kontrak yang sama jika semantik ikut berubah.

Nullability dan field wajib

Mengubah field opsional menjadi wajib hampir selalu breaking. Demikian juga mengubah field yang dulunya selalu string menjadi kadang null. Banyak klien gagal bukan karena tidak ada data, tetapi karena deserializer atau validasi lokal tidak siap menerima bentuk baru.

Perubahan Perilaku Auth tanpa Memutus Klien Lama

Perubahan autentikasi sering dianggap terpisah dari kontrak API, padahal bagi klien integrasi itu bagian dari kontrak operasional. Jika sebelumnya klien mengirim:

Authorization: Bearer <legacy-token>

lalu Anda mewajibkan skema signature baru, mTLS, atau header tambahan, itu adalah breaking change kecuali ada masa transisi yang jelas.

Strategi migrasi auth yang lebih aman

  • Dukung skema lama dan baru secara paralel untuk jangka waktu tertentu.
  • Kaitkan kemampuan auth dengan versi kontrak atau profil integrasi.
  • Tambahkan endpoint introspeksi atau health check untuk membantu klien memverifikasi kredensial baru sebelum cutover.
  • Log alasan penolakan auth secara terstruktur, misalnya expired_token, missing_signature, atau unsupported_auth_scheme.

Contoh pendekatan paralel:

if request uses signature-v2:
  verifySignatureV2(request)
else if request uses bearer legacy:
  verifyLegacyBearer(request)
else:
  reject(401, "unsupported_auth_scheme")

Ketika dua mode auth hidup bersamaan, pastikan prioritas evaluasi dan pesan error konsisten agar debugging klien tidak membingungkan.

Webhook: Bagian yang Paling Sering Terlupakan

Banyak tim hati-hati pada endpoint request/response, tetapi memecahkan integrasi lewat webhook. Webhook adalah kontrak keluar dari server ke klien; ia juga harus di-version.

Prinsip kompatibilitas webhook

  • Versi webhook harus dipin saat endpoint webhook didaftarkan.
  • Payload event lama tidak boleh berubah diam-diam.
  • Signature, timestamp, dan skema retry harus stabil selama versi yang sama.
  • Jika event baru diperkenalkan, bedakan antara event type baru dan perubahan payload event lama.

Contoh payload webhook:

// webhook v1
{
  "type": "invoice.paid",
  "data": {
    "id": "inv_123",
    "status": "paid"
  }
}

// webhook v2
{
  "type": "invoice.payment_succeeded",
  "data": {
    "id": "inv_123",
    "payment": {
      "status": "succeeded"
    }
  }
}

Jika klien mendaftar webhook pada versi v1, terus kirim bentuk v1 sampai mereka memigrasikan endpoint webhook tersebut. Jangan mengganti payload hanya karena API request mereka sudah memakai versi baru; banyak organisasi memisahkan tim consumer webhook dan tim consumer API sinkron.

Retry dan idempotency saat dua versi hidup bersamaan

Saat beberapa versi kontrak aktif, sistem retry dapat menghasilkan kejadian yang sulit dilacak:

  • event yang sama terkirim ke dua endpoint dengan versi payload berbeda,
  • request retried oleh klien lama setelah server internal sudah memakai representasi baru,
  • duplikasi terjadi ketika proses migrasi mengaktifkan endpoint baru sebelum endpoint lama dimatikan.

Karena itu:

  • gunakan idempotency key untuk operasi tulis dari klien,
  • berikan event id unik pada webhook,
  • simpan status delivery per endpoint dan per versi,
  • pastikan retry mengirim ulang payload yang sama untuk versi yang sama, bukan hasil serialisasi terbaru yang mungkin berbeda.

Contoh request idempotent:

POST /payments
Idempotency-Key: 8f4f3b7e-9a7f-4d41-a6d8-1f4f0b7f8c10
API-Version: 2024-10-01

Kunci idempotensi sebaiknya diproses bersama konteks kontrak. Jangan sampai request yang sama pada versi berbeda dianggap identik bila hasil bisnis atau bentuk responsnya berbeda.

Matriks Kompatibilitas yang Sebaiknya Dimiliki

Dokumentasi naratif saja tidak cukup. Simpan matriks yang menunjukkan perilaku sistem pada setiap versi kontrak.

Areav1 / 2024-10-01v2 / 2025-03-01
Field status invoicestatus di rootpayment.status
Enum suksespaidsucceeded
Timestampopsional, ISO-8601wajib untuk event tertentu
AuthBearer legacy didukungSignature v2 direkomendasikan, legacy opsional selama transisi
Webhook eventinvoice.paidinvoice.payment_succeeded
Format errormessage sederhanacode + details
Idempotencyopsional pada endpoint tertentuwajib pada operasi pembayaran

Matriks seperti ini membantu engineering, support, QA, dan integrator berbicara dengan istilah yang sama.

Strategi Deprecate yang Terukur

Deprecation bukan email massal semata. Ia harus punya inventaris, observabilitas, fase, dan kriteria keberhasilan.

Tahapan yang realistis

  1. Umumkan versi baru beserta alasan dan panduan migrasi.
  2. Pin klien baru ke versi baru, tetapi pertahankan versi lama untuk klien eksisting.
  3. Ukur siapa yang masih memakai versi lama: per API key, endpoint, traffic, dan error rate.
  4. Beri peringatan melalui dashboard, email teknis, response header, atau webhook khusus status integrasi.
  5. Sediakan sandbox atau mode uji untuk versi baru.
  6. Tentukan tanggal akhir hanya setelah data pemakaian cukup jelas.
  7. Lakukan cutover bertahap dan monitor error pasca-perubahan.

Header deprecation yang membantu

Jika infrastruktur Anda mendukung, response dapat mengandung sinyal seperti tanggal penghentian atau tautan migrasi. Yang penting adalah konsistensi dan keterbacaan oleh klien maupun tim support.

Jangan menandai sesuatu deprecated jika Anda belum punya jalur migrasi yang setara secara fungsional. “Deprecated” tanpa alternatif praktis hanya memindahkan masalah ke klien.

Checklist Review Perubahan Kontrak API

Sebelum merilis perubahan, tinjau dengan checklist berikut:

  • Apakah ada field yang dihapus, dipindah, atau diganti tipe datanya?
  • Apakah ada field opsional yang menjadi wajib?
  • Apakah nilai enum bertambah, berubah nama, atau berubah makna?
  • Apakah format tanggal, angka, mata uang, atau precision berubah?
  • Apakah kode error, status HTTP, atau body error berubah?
  • Apakah parser klien yang ketat bisa gagal karena field tambahan?
  • Apakah perubahan auth memerlukan header, signature, scope, atau kredensial baru?
  • Apakah webhook, event type, signature, atau urutan event ikut berubah?
  • Apakah retry dan idempotency tetap konsisten saat dua versi berjalan bersamaan?
  • Apakah perubahan ini additive atau sebenarnya breaking?
  • Jika breaking, bagaimana klien memilih kontrak baru secara eksplisit?
  • Apakah monitoring dapat memisahkan traffic dan error per versi?
  • Apakah dokumentasi memuat before/after, bukan hanya definisi akhir?

Anti-pattern Umum pada Integrasi Pihak Ketiga

Mengubah default diam-diam

Ini anti-pattern paling berbahaya. Server mulai mengirim payload baru tanpa opt-in klien, lalu menganggap klien “harusnya fleksibel”. Pada integrasi pihak ketiga, asumsi ini mahal karena Anda tidak mengontrol kode dan jadwal rilis mereka.

Semua perubahan dipaksa ke /v2 besar

Versi mayor tunggal untuk seluruh API sering memicu migrasi besar yang lambat. Jika perubahan hanya terjadi pada beberapa area, pertimbangkan kontrak berbasis capability atau version pinning per resource family.

Menambah enum tanpa fallback policy

Secara teori additive, secara praktik sering breaking. Banyak switch-case di klien tidak punya cabang default yang aman.

Webhook ikut berubah mengikuti versi API request

Request sinkron dan webhook asinkron adalah dua integrasi berbeda. Menyamakan versinya secara implisit sering membuat satu tim memperbaiki bug baru yang tidak mereka picu.

Retry menghasilkan representasi baru

Jika request atau webhook di-retry, payload ulang harus identik untuk kontrak dan event yang sama. Mengirim representasi terbaru setelah model internal berubah membuat debugging hampir mustahil.

Deprecation tanpa data pemakaian

Anda tidak bisa menghentikan versi lama dengan aman jika tidak tahu siapa yang masih memakainya, endpoint mana yang aktif, dan kegagalan apa yang akan terjadi.

Contoh Desain yang Lebih Tahan Lama

Pola berikut cukup praktis untuk banyak sistem integrasi:

  1. Setiap klien memiliki pinned API contract version.
  2. Klien boleh override versi per request melalui header eksplisit.
  3. Serializer response memilih shape berdasarkan versi ter-resolve.
  4. Validator request juga membaca versi yang sama.
  5. Webhook endpoint menyimpan versinya sendiri saat registrasi.
  6. Idempotency store menyertakan konteks versi.
  7. Metric, log, dan tracing selalu mencatat versi kontrak.
request_context = {
  client_id: "cli_123",
  contract_version: "2024-10-01",
  auth_scheme: "bearer-legacy",
  idempotency_key: "8f4f3b7e-..."
}

response = serializer.forVersion(request_context.contract_version).render(resource)

Dengan model ini, Anda memisahkan evolusi domain internal dari kontrak eksternal. Database, service internal, atau event bus boleh berubah, tetapi lapisan kontrak memastikan klien lama tetap menerima bentuk yang dijanjikan.

Penutup

Versioning kontrak API tanpa merusak integrasi klien lama bukan soal menambah angka versi semata, melainkan soal disiplin kompatibilitas. Perubahan besar harus hadir sebagai mode eksplisit yang dipilih klien, bukan sebagai default baru yang diam-diam menggantikan kontrak lama.

Jika Anda menerapkan prinsip ini secara konsisten pada schema, enum, auth, webhook, retry, dan deprecation, evolusi API akan jauh lebih terkendali. Tim internal tetap bisa bergerak, sementara integrasi pihak ketiga tidak menjadi korban perubahan yang seharusnya bisa diisolasi sejak awal.