Versioning kontrak API adalah cara paling praktis untuk mencegah integrasi klien rusak saat skema berubah. Masalah utamanya bukan sekadar menambah field baru, tetapi memastikan perubahan format, enum, nullability, dan struktur payload tetap bisa diproses oleh klien lama selama masa transisi.

Untuk API REST dan webhook, pendekatan yang aman biasanya menggabungkan beberapa hal: definisi kontrak yang eksplisit, klasifikasi perubahan menjadi additive atau breaking, jendela kompatibilitas yang jelas, kebijakan deprecasi, validasi skema di CI, serta rollout bertahap. Konteks seperti rilis jj v0.43.0 mengingatkan bahwa perubahan yang dikelola dengan baik lebih penting daripada perubahan yang cepat: saat antarmuka berubah, konsumen butuh jalur migrasi yang stabil, bukan kejutan.

Referensi konteks: jj v0.43.0 release notes. Artikel ini tidak membahas rilis tersebut sebagai berita, melainkan menggunakan semangat perubahan terkelola sebagai inspirasi desain kontrak API.

Mengapa kontrak API mudah merusak integrasi

Integrasi rusak sering terjadi bukan karena endpoint hilang, tetapi karena asumsi klien terhadap payload berubah tanpa koordinasi. Beberapa contoh umum:

  • Field diganti nama dari full_name menjadi name.
  • Field yang dulu selalu ada sekarang kadang null.
  • Nilai enum baru ditambahkan, tetapi parser klien hanya mengenal daftar lama.
  • Tipe data berubah, misalnya ID numerik menjadi string.
  • Struktur nested berubah, misalnya customer.email dipindah ke contact.email.

Semua perubahan di atas tampak kecil dari sisi server, tetapi bisa memicu kegagalan deserialisasi, bug logika, atau data hilang di sisi konsumen. Karena itu, kontrak API harus diperlakukan seperti antarmuka publik yang memiliki siklus hidup.

Klasifikasi perubahan: additive vs breaking change

Langkah pertama dalam versioning kontrak API adalah membedakan perubahan yang aman dari yang berisiko merusak klien.

Perubahan additive

Perubahan additive menambah informasi tanpa mengubah arti data lama. Contohnya:

  • Menambah field baru opsional.
  • Menambah endpoint baru tanpa mengubah endpoint lama.
  • Menambah header respons yang tidak wajib dipakai klien.

Contoh payload sebelum dan sesudah perubahan additive:

{
  "id": "ord_123",
  "status": "paid",
  "amount": 150000
}
{
  "id": "ord_123",
  "status": "paid",
  "amount": 150000,
  "currency": "IDR"
}

Jika klien mengabaikan field yang tidak dikenal, perubahan ini biasanya aman.

Perubahan breaking

Perubahan breaking mengubah interpretasi, struktur, atau kewajiban pemrosesan payload. Contohnya:

  • Menghapus field.
  • Mengganti nama field.
  • Mengubah tipe data.
  • Mengubah nilai enum yang mungkin muncul.
  • Mengubah field opsional menjadi wajib, atau sebaliknya dengan dampak semantik.

Contoh breaking change:

{
  "id": "ord_123",
  "status": "paid",
  "amount": 150000
}
{
  "id": "ord_123",
  "payment_status": "settled",
  "amount": "150000"
}

Di sini ada tiga masalah sekaligus: status diganti nama, nilai enum berubah dari paid menjadi settled, dan amount berubah dari number ke string. Bagi klien yang mengandalkan kontrak lama, ini jelas merusak.

Prinsip desain kontrak yang tahan perubahan

1. Perlakukan rename sebagai breaking change

Rename field sering dianggap remeh karena datanya “sama”. Padahal parser, mapper, dan query klien biasanya bergantung pada nama field secara langsung. Jika ingin migrasi aman, kirim kedua field untuk sementara, dokumentasikan prioritasnya, lalu hapus field lama setelah masa deprecasi.

Contoh transisi yang lebih aman:

{
  "id": "ord_123",
  "status": "paid",
  "payment_status": "paid"
}

Setelah semua klien bermigrasi, baru versi baru dapat menghapus status.

2. Hati-hati dengan enum

Menambah nilai enum baru sering dianggap additive, tetapi di banyak klien hal ini bisa menjadi breaking secara praktis. Misalnya klien memiliki switch yang hanya mengenal pending, paid, dan failed. Ketika server mulai mengirim refunded, logika klien bisa gagal.

Karena itu:

  • Dokumentasikan bahwa klien harus menangani nilai enum tak dikenal.
  • Sediakan nilai fallback seperti unknown di domain model klien.
  • Hindari mengganti arti enum lama tanpa versi baru.

3. Nullability adalah bagian dari kontrak

Perubahan dari non-null menjadi nullable bisa merusak kode yang tidak menyiapkan pengecekan null. Sebaliknya, mengubah field nullable menjadi wajib juga berisiko bagi klien yang belum mengirimkannya.

Anggap aturan berikut sebagai aman:

  • Response: jangan ubah field yang tadinya selalu ada menjadi kadang null tanpa versi atau masa transisi.
  • Request: jangan ubah field opsional menjadi wajib di versi yang sama.

4. Gunakan semantik yang stabil, bukan hanya bentuk JSON yang stabil

Dua payload bisa tampak mirip tetapi bermakna berbeda. Misalnya field amount dulu berarti total kotor, lalu diam-diam berubah menjadi total bersih. Ini breaking meskipun nama dan tipe field tidak berubah. Versioning harus mempertimbangkan makna bisnis, bukan hanya struktur.

Memilih strategi versioning: path, header, atau media type

Tidak ada satu strategi yang selalu paling benar. Yang penting adalah konsistensi, kemudahan observabilitas, dan kemampuan menjalankan beberapa versi secara paralel.

Versioning di path

GET /api/v1/orders/ord_123
GET /api/v2/orders/ord_123

Kelebihan:

  • Mudah dipahami manusia.
  • Mudah dirouting di gateway, reverse proxy, dan observability.
  • Mudah menjalankan dua implementasi paralel.

Kekurangan:

  • URL berubah meski resource secara konsep sama.
  • Bisa mendorong duplikasi endpoint jika semua perubahan kecil langsung jadi versi baru.

Versioning lewat header

GET /api/orders/ord_123
API-Version: 2024-10-01

Kelebihan:

  • URL tetap bersih dan stabil.
  • Cocok bila versi dianggap bagian dari negosiasi kontrak, bukan identitas resource.

Kekurangan:

  • Lebih sulit diuji manual jika tooling tim belum terbiasa.
  • Sering kurang terlihat di log atau dashboard jika header tidak dicatat dengan baik.

Media type versioning

Accept: application/vnd.example.orders+json;version=2

Kelebihan:

  • Secara konsep rapi untuk content negotiation.
  • Berguna jika representasi resource sangat bervariasi.

Kekurangan:

  • Lebih kompleks untuk banyak tim.
  • Sering berlebihan untuk API internal atau integrasi sederhana.

Kapan memilih yang mana?

Untuk banyak tim backend, path versioning paling praktis untuk perubahan breaking besar pada REST API. Untuk webhook, lebih umum memakai version per endpoint subscription atau header metadata versi event, karena pengirim yang mengontrol payload perlu tahu format apa yang diharapkan konsumen.

Yang lebih penting dari pilihan mekanisme adalah aturan ini:

  • Perubahan additive tidak harus selalu membuat versi baru.
  • Perubahan breaking harus punya versi baru atau compatibility layer yang jelas.
  • Satu klien harus bisa tetap di versi lama selama jendela kompatibilitas.

Webhook perlu disiplin lebih ketat daripada REST

Pada REST, klien aktif meminta data. Pada webhook, server mendorong data ke konsumen. Artinya, jika payload webhook berubah mendadak, konsumen tidak punya kontrol penuh atas timing perubahan. Karena itu, webhook sebaiknya memiliki kontrak yang lebih konservatif.

Gunakan versi pada subscription atau endpoint webhook

Contoh pendekatan:

  • Konsumen mendaftarkan endpoint webhook dengan versi tertentu, misalnya 2024-10-01.
  • Server mengirim payload sesuai versi itu sampai konsumen memperbarui subscription.

Contoh metadata event:

{
  "event_id": "evt_9f3a",
  "event_type": "order.paid",
  "api_version": "2024-10-01",
  "occurred_at": "2024-10-12T09:15:00Z",
  "data": {
    "id": "ord_123",
    "status": "paid",
    "amount": 150000
  }
}

Dengan begitu, tim integrasi dapat memproses event berdasarkan versi kontrak yang disepakati, bukan menebak-nebak bentuk payload saat runtime.

Idempotency penting untuk retry aman

Webhook hampir pasti akan di-retry jika endpoint penerima lambat, timeout, atau mengembalikan status non-2xx. Karena itu, payload perlu memiliki identifier unik yang stabil, dan penerima perlu memprosesnya secara idempoten.

Praktik yang umum:

  • Sertakan event_id unik pada setiap event.
  • Penerima menyimpan event_id yang sudah diproses.
  • Jika event yang sama datang lagi, balas sukses tanpa memproses ulang efek samping.

Contoh pseudocode penerima webhook:

if event_id sudah ada di storage:
  return 200

validasi signature
validasi schema sesuai api_version
jalankan proses bisnis
simpan event_id sebagai processed
return 200

Ini mencegah order diproses dua kali, email dikirim ulang, atau mutasi data ganda saat retry terjadi.

Compatibility window dan deprecation policy

Versioning yang baik bukan hanya soal menambahkan /v2, tetapi juga memberi waktu migrasi yang realistis. Tanpa compatibility window, versi baru hanya memindahkan masalah dari desain ke operasional.

Tentukan jendela kompatibilitas

Beberapa prinsip praktis:

  • Tetapkan berapa lama versi lama tetap didukung setelah versi baru tersedia.
  • Pastikan klien bisa menguji versi baru sebelum cut-off.
  • Untuk webhook, izinkan konsumen memilih kapan pindah versi selama masa dukungan.

Durasi pastinya tergantung jenis pelanggan, SLA, dan jumlah integrasi. Tidak perlu menetapkan angka sembarangan; yang penting adalah jelas, terdokumentasi, dan konsisten.

Buat kebijakan deprecasi yang operasional

Deprecation policy sebaiknya menjawab empat hal:

  1. Apa yang berubah.
  2. Siapa yang terdampak.
  3. Kapan perilaku lama berhenti didukung.
  4. Langkah migrasi yang diperlukan.

Contoh informasi yang perlu diumumkan:

  • Field status akan digantikan oleh payment_status.
  • Mulai tanggal tertentu, field lama ditandai deprecated tetapi masih dikirim.
  • Pada versi baru, hanya field baru yang tersedia.
  • Contoh payload, daftar perubahan enum, dan aturan nullability baru.

Kesalahan umum adalah mengumumkan deprecasi di changelog, tetapi tidak menambahkan sinyal di dokumentasi, dashboard integrasi, log, atau komunikasi ke pemilik integrasi.

Schema validation dan contract testing

Dokumentasi saja tidak cukup. Untuk mencegah perubahan breaking lolos ke produksi, kontrak harus diuji otomatis.

Validasi skema untuk request dan response

Gunakan skema yang eksplisit, misalnya OpenAPI atau JSON Schema, lalu validasi:

  • Request yang masuk ke server.
  • Response yang keluar dari server.
  • Payload webhook yang diproduksi.

Tujuannya bukan membuat sistem kaku, melainkan memastikan perubahan yang tidak disengaja segera terdeteksi. Validasi sangat berguna untuk kasus seperti:

  • Field wajib tiba-tiba hilang.
  • Tipe data berubah tanpa sadar.
  • Enum baru muncul tanpa pembaruan kontrak.

Gunakan contract diff di CI

Saat ada pull request yang mengubah skema, jalankan pemeriksaan yang membandingkan kontrak lama dan baru. Tinjau apakah perubahan bersifat additive atau breaking. Jika breaking, CI sebaiknya memaksa pengembang untuk:

  • Menandai versi baru, atau
  • Menambahkan compatibility layer, atau
  • Mendapatkan persetujuan eksplisit dengan rencana rollout.

Meski tool yang dipakai bisa berbeda-beda, prinsipnya sama: jangan mengandalkan review manual semata.

Consumer-driven contract bila integrasi banyak

Jika ada banyak konsumen dengan kebutuhan berbeda, pertimbangkan pendekatan consumer-driven contract. Konsumen mendefinisikan ekspektasi minimumnya, lalu provider memverifikasi bahwa perubahan server tidak melanggar ekspektasi itu. Pendekatan ini berguna terutama untuk API internal antarlayanan.

Strategi rollout bertahap yang aman

Perubahan kontrak yang benar secara desain tetap bisa gagal jika rollout-nya kasar. Jalur yang lebih aman biasanya bertahap.

Urutan rollout yang disarankan

  1. Tambahkan field atau versi baru tanpa menghapus yang lama.
  2. Perbarui dokumentasi dan contoh payload.
  3. Aktifkan validasi dan logging untuk mendeteksi pemakaian versi lama.
  4. Uji dengan klien internal atau sandbox.
  5. Izinkan opt-in ke versi baru.
  6. Pantau error rate, parsing failure, dan retry webhook.
  7. Setelah semua konsumen utama bermigrasi, baru nonaktifkan kontrak lama.

Observabilitas yang perlu disiapkan

  • Log versi kontrak yang dipakai per request atau event.
  • Hitung distribusi trafik per versi.
  • Lacak validation error per field.
  • Pisahkan metrik retry webhook dan duplicate event handling.

Tanpa observabilitas, tim biasanya tidak tahu klien mana yang masih bergantung pada kontrak lama sampai insiden terjadi.

Contoh evolusi payload yang aman

Skenario: mengganti nama field dan menambah detail baru

Payload lama:

{
  "id": "ord_123",
  "status": "paid",
  "customer": {
    "full_name": "Budi Santoso"
  }
}

Target desain baru:

{
  "id": "ord_123",
  "payment_status": "paid",
  "customer": {
    "name": "Budi Santoso",
    "email": "[email protected]"
  }
}

Jika langsung diganti, ini breaking. Jalur migrasi yang lebih aman pada masa transisi:

{
  "id": "ord_123",
  "status": "paid",
  "payment_status": "paid",
  "customer": {
    "full_name": "Budi Santoso",
    "name": "Budi Santoso",
    "email": "[email protected]"
  }
}

Lalu dokumentasikan:

  • status deprecated, gunakan payment_status.
  • customer.full_name deprecated, gunakan customer.name.
  • Versi berikutnya akan menghapus field deprecated.

Strategi ini bekerja karena klien lama tetap berjalan, sementara klien baru bisa segera pindah tanpa menunggu cut-over besar.

Checklist review perubahan kontrak API

Sebelum merilis perubahan pada REST API atau webhook, gunakan checklist ini:

  • Apakah ada field yang dihapus, diganti nama, dipindah, atau diubah tipenya?
  • Apakah ada perubahan enum, termasuk penambahan nilai baru?
  • Apakah nullability berubah?
  • Apakah arti bisnis suatu field berubah meski nama dan tipe tetap?
  • Apakah request lama masih valid?
  • Apakah response lama masih bisa diparse klien lama?
  • Apakah webhook retry tetap aman karena idempotency sudah diterapkan?
  • Apakah kontrak tervalidasi otomatis di CI?
  • Apakah ada compatibility window dan tanggal deprecasi yang jelas?
  • Apakah dokumentasi, contoh payload, dan log versi sudah diperbarui?

Kesalahan umum yang sering menimbulkan insiden

"Hanya rename field"

Ini hampir selalu breaking. Jangan anggap aman hanya karena datanya sama.

Menambah enum tanpa fallback

Jika klien tidak siap menerima nilai tak dikenal, penambahan enum baru bisa mematahkan alur bisnis.

Mengubah nullability diam-diam

Field yang tiba-tiba null sering lolos tes unit tetapi gagal di produksi saat data nyata bervariasi.

Mengandalkan dokumentasi tanpa validasi otomatis

Kontrak yang tidak diuji akan menyimpang seiring waktu. Dokumentasi mudah ketinggalan dibanding implementasi.

Menghapus versi lama terlalu cepat

Tim server sering merasa migrasi sudah selesai, padahal ada klien batch, integrasi partner, atau worker lama yang masih aktif.

Penutup

Versioning kontrak API yang baik bukan berarti membuat versi baru untuk setiap perubahan, melainkan membedakan perubahan additive dan breaking dengan disiplin, lalu menyediakan jalur migrasi yang bisa dijalankan klien tanpa insiden. Untuk REST dan webhook, kombinasi yang paling berguna biasanya adalah kontrak eksplisit, compatibility window, deprecation policy, validasi skema, idempotency untuk retry aman, dan rollout bertahap.

Jika harus memilih satu prinsip inti, pilih ini: jangan pernah memaksa semua klien berubah pada saat yang sama. Desain kontrak yang baik memberi ruang transisi, observabilitas, dan kepastian perilaku saat skema berkembang.