Versioning API tanpa merusak client berarti perubahan kontrak harus dirancang agar konsumen lama tetap bisa berfungsi selama masa transisi. Masalahnya bukan sekadar menambah /v2 di URL, tetapi memastikan perubahan field, enum, format tanggal, error shape, dan perilaku default tidak diam-diam mematahkan asumsi di sisi client.

Dalam praktiknya, banyak kegagalan integrasi terjadi bukan karena server down, melainkan karena kontrak berubah tanpa strategi kompatibilitas: field dihapus, enum baru muncul, format tanggal berganti, atau default behavior berubah. Artikel ini fokus pada cara mengelola perubahan tersebut secara aman: membedakan breaking dan non-breaking change, memilih pendekatan versioning, menyiapkan deprecation policy, mengirim Sunset header, menyediakan fallback, dan memantau client lama sebelum benar-benar mematikan versi lama.

API contract: yang sebenarnya dijaga bukan endpoint, tetapi ekspektasi client

API contract adalah kesepakatan antara server dan client: struktur request/response, tipe data, aturan field wajib, nilai enum, format tanggal, kode status, bentuk error, pagination, sampai perilaku default ketika parameter tidak dikirim. Contract inilah yang harus dianggap stabil.

Kesalahan umum adalah menganggap perubahan kecil pada JSON selalu aman. Padahal aman atau tidak bergantung pada cara client mengonsumsi data. Client yang robust biasanya mengabaikan field tambahan dan hanya membaca field yang diperlukan. Tetapi banyak integrasi partner menggunakan parser ketat, mapping manual, atau validasi schema yang membuat perubahan kecil menjadi fatal.

Contoh kontrak yang sering dianggap sepele tetapi berisiko

  • Menghapus field meski dianggap sudah tidak dipakai.
  • Mengganti nama field tanpa alias atau masa transisi.
  • Mengubah tipe data, misalnya amount dari string menjadi number.
  • Mengubah format tanggal, misalnya dari 2025-05-01 10:00:00 ke ISO 8601 2025-05-01T10:00:00Z.
  • Menambah nilai enum baru yang tidak dikenal client lama.
  • Mengubah default sorting/filtering sehingga hasil bisnis berbeda walau request sama.
  • Mengubah shape error dari string menjadi object bertingkat.

Prinsip praktis: jika ada kemungkinan client lama menghasilkan error, salah parsing, atau mengambil keputusan bisnis yang berbeda untuk request yang sama, anggap itu sebagai perubahan kontrak yang harus dikelola secara eksplisit.

Breaking change vs non-breaking change dalam versioning API

Tidak semua perubahan harus membuat versi baru. Namun tidak semua perubahan kecil aman untuk dirilis tanpa mitigasi.

Perubahan yang biasanya non-breaking

  • Menambah field baru yang benar-benar opsional di response.
  • Menambah endpoint baru.
  • Menambah query parameter baru yang opsional.
  • Menambah metadata baru selama client lama tetap bisa mengabaikannya.

Meski demikian, ini hanya aman jika client memang toleran terhadap field tambahan. Jika banyak partner memakai schema validation dengan additionalProperties: false atau deserializer ketat, penambahan field pun bisa menjadi breaking secara operasional.

Perubahan yang biasanya breaking

  • Menghapus field, mengganti nama field, atau memindahkan field ke nesting baru.
  • Mengubah tipe data field.
  • Mengubah format tanggal/waktu atau satuan angka.
  • Mengubah arti nilai yang sudah ada.
  • Menambah nilai enum tanpa dokumentasi dan fallback yang jelas.
  • Mengubah kode status HTTP yang diandalkan client.
  • Mengubah syarat field wajib pada request.

Edge case nyata: enum baru yang mematahkan partner

Misalnya API order awalnya hanya mengembalikan status berikut:

{
  "order_id": "ORD-1001",
  "status": "paid"
}

Partner menulis kode seperti ini:

switch (response.status) {
  case "pending":
  case "paid":
  case "cancelled":
    processOrder(response);
    break;
  default:
    throw new Error("Unknown status");
}

Kemudian server menambah status baru refunded. Dari sudut pandang provider API, ini terlihat seperti penambahan non-breaking. Dari sudut pandang partner, integrasi gagal total karena parser bisnis menganggap status baru sebagai error.

Pelajarannya: stabilitas kontrak bukan hanya soal schema, tetapi juga soal asumsi yang wajar dibuat client. Untuk field enum penting, dokumentasikan bahwa client harus siap menerima nilai tak dikenal, atau sediakan strategi fallback seperti memetakan status baru ke kategori lama selama masa transisi.

Pendekatan versioning: path, header, dan version per contract

Tidak ada satu pendekatan yang selalu paling benar. Yang penting adalah konsistensi, kemudahan observasi, dan kemampuan mengelola migrasi.

Path versioning: paling mudah dilihat dan dipantau

Contoh:

GET /v1/orders/ORD-1001
GET /v2/orders/ORD-1001

Kelebihan:

  • Mudah dipahami partner dan tim internal.
  • Mudah diroute di gateway, CDN, logging, dan dashboard.
  • Mudah memisahkan dokumentasi dan SLA per versi.

Kekurangan:

  • Cenderung membuat duplikasi endpoint jika perubahan kecil sebenarnya bisa diatasi dengan evolusi schema.
  • Versi sering dianggap identik dengan seluruh API, padahal perubahan bisa hanya menyentuh sebagian resource.

Header versioning: lebih fleksibel, tapi observabilitas harus disiplin

Contoh:

GET /orders/ORD-1001
Accept: application/vnd.example.order+json;version=2

Atau menggunakan header khusus:

API-Version: 2

Kelebihan:

  • URL lebih stabil.
  • Cocok jika versi berbeda hanya memengaruhi representasi resource, bukan identitas endpoint.

Kekurangan:

  • Lebih sulit didiagnosis jika logging/proxy tidak menyimpan header dengan baik.
  • Partner sering lupa mengirim header yang benar saat uji manual atau saat ada komponen perantara.
  • Cache dan tooling lebih mudah salah konfigurasi jika variasi response berdasarkan header tidak diperhitungkan.

Kapan memilih yang mana?

  • Pilih path versioning jika Anda butuh kejelasan operasional, banyak partner eksternal, dan ingin mendeteksi trafik per versi dengan mudah.
  • Pilih header versioning jika Anda mengelola representasi resource yang berbeda namun ingin URL tetap konsisten, dan tim Anda sudah matang dalam logging, caching, dan API gateway.

Yang paling penting: versi harus merepresentasikan kontrak, bukan sekadar milestone rilis. Hindari membuat versi baru untuk setiap perubahan kecil jika kompatibilitas masih bisa dijaga.

Schema evolution yang aman: tambah, alias, lalu migrasikan

Perubahan kontrak yang aman biasanya mengikuti pola evolusi bertahap, bukan penggantian mendadak.

Contoh perubahan field: sebelum dan sesudah

Versi lama:

{
  "id": "ORD-1001",
  "created_at": "2025-05-01 10:00:00",
  "customer_name": "Rina",
  "status": "paid"
}

Tim backend ingin:

  • Mengganti created_at ke ISO 8601.
  • Memecah customer_name menjadi object customer.
  • Menambah status baru.

Jika langsung diubah menjadi:

{
  "id": "ORD-1001",
  "created_at": "2025-05-01T10:00:00Z",
  "customer": {
    "name": "Rina"
  },
  "status": "refunded"
}

ini berpotensi mematahkan banyak client lama.

Strategi yang lebih aman untuk periode transisi:

{
  "id": "ORD-1001",
  "created_at": "2025-05-01 10:00:00",
  "created_at_iso": "2025-05-01T10:00:00Z",
  "customer_name": "Rina",
  "customer": {
    "name": "Rina"
  },
  "status": "paid",
  "status_detail": "refunded"
}

Pendekatan ini menjaga kompatibilitas dengan beberapa cara:

  • created_at lama tetap ada sampai client siap pindah ke created_at_iso.
  • customer_name tetap tersedia sambil client baru memakai object customer.
  • status tetap di nilai lama yang kompatibel secara bisnis, sementara granularitas baru ditaruh di status_detail.

Ini bukan solusi permanen. Tujuannya memberi ruang migrasi tanpa mematahkan integrasi produksi.

Trade-off dari alias dan field ganda

  • Response menjadi lebih besar dan dokumentasi lebih kompleks.
  • Risiko inkonsistensi meningkat jika dua field yang merepresentasikan data sama tidak selalu sinkron.
  • Tim bisa lupa menghapus field lama setelah masa deprecation berakhir.

Karena itu, setiap alias atau field transisi harus punya tanggal akhir dan owner yang jelas.

Deprecation policy, Sunset header, dan komunikasi yang bisa diotomatisasi

Jika perubahan memang breaking, jangan hanya mengumumkannya di changelog. Client perlu sinyal teknis langsung di response, agar mereka bisa mendeteksi bahwa versi atau field tertentu sedang menuju penghentian.

Elemen minimum deprecation policy

  • Apa yang deprecated: endpoint, field, versi, atau perilaku tertentu.
  • Kapan diumumkan.
  • Kapan tidak lagi didukung.
  • Apa pengganti resminya.
  • Dampak jika tidak migrasi.
  • Kontak/support path untuk partner terdampak.

Contoh header deprecation dan sunset

Deprecation: true
Sunset: Wed, 31 Dec 2025 23:59:59 GMT
Link: <https://developer.example.com/docs/orders-v2-migration>; rel="deprecation"

Maknanya:

  • Deprecation memberi tahu bahwa resource atau perilaku ini sudah memasuki fase penghentian.
  • Sunset memberi tanggal kapan dukungan akan dihentikan.
  • Link mengarahkan client ke panduan migrasi yang relevan.

Kenapa ini penting? Karena banyak client enterprise tidak terus-menerus membaca changelog, tetapi mereka bisa menangkap header pada monitoring atau log pipeline mereka. Ini mengubah pengumuman pasif menjadi sinyal yang dapat diobservasi.

Kesalahan umum saat menjalankan sunset

  • Memberi tanggal terlalu dekat dengan waktu pengumuman.
  • Mengirim header sunset tetapi belum menyediakan dokumentasi migrasi yang jelas.
  • Menghapus versi lama tepat pada tanggal sunset tanpa memeriksa apakah masih ada trafik kritis.
  • Tidak membedakan antara traffic uji, partner sandbox, dan partner produksi.

Fallback behavior: alat transisi, bukan alasan menunda desain kontrak

Fallback behavior adalah cara server tetap melayani client lama ketika perubahan kontrak atau perilaku sedang berjalan. Fallback berguna untuk mengurangi risiko saat rollout, tetapi harus dibatasi agar tidak menjadi beban permanen.

Bentuk fallback yang umum

  • Field alias: mengirim field lama dan baru secara bersamaan.
  • Enum mapping: memetakan nilai baru ke kategori lama untuk client versi lama.
  • Format fallback: mempertahankan format tanggal lama sambil menambahkan field ISO baru.
  • Behavior fallback: mempertahankan default sorting/filter lama untuk versi tertentu.
  • Error fallback: untuk client lama, tetap kirim shape error lama walau sistem internal sudah berubah.

Contoh fallback berdasarkan versi

if client_version == 1:
  response.status = map_new_status_to_legacy(order.status)
  response.created_at = format_legacy_datetime(order.created_at)
else:
  response.status = order.status
  response.created_at = format_iso8601(order.created_at)

Trade-off-nya jelas: logic server menjadi lebih kompleks, pengujian bertambah, dan kemungkinan bug meningkat. Karena itu fallback sebaiknya:

  • Dibatasi per versi atau per partner yang jelas.
  • Memiliki metrik penggunaan.
  • Memiliki tanggal penghapusan.
  • Diuji dengan contract test, bukan hanya unit test.

Kapan fallback justru berbahaya?

  • Jika fallback menyembunyikan masalah bisnis, misalnya status refunded dipetakan ke paid sehingga partner salah mengambil keputusan finansial.
  • Jika fallback menghasilkan data ambigu karena dua field berbeda nilainya.
  • Jika fallback hanya berlaku di sebagian endpoint sehingga perilaku tidak konsisten.

Gunakan fallback untuk kompatibilitas teknis, bukan untuk menyamarkan perubahan domain yang signifikan.

Kasus nyata: partner gagal karena field, enum, dan format tanggal berubah

1. Field dihapus karena dianggap sudah usang

Provider menghapus customer_name karena sudah ada customer.name. Partner lama punya template invoice yang mengambil customer_name langsung. Akibatnya invoice kosong, meski request tetap sukses 200 OK.

Pelajaran: perubahan kontrak tidak selalu memunculkan error HTTP. Banyak kerusakan justru berupa data yang diam-diam salah.

2. Enum baru mematahkan switch statement

Status baru partially_refunded muncul. Client lama memiliki validasi ketat yang hanya mengenal pending, paid, dan cancelled. Hasilnya proses order berhenti di jalur exception.

Mitigasi:

  • Dokumentasikan enum sebagai open set jika memang bisa berkembang.
  • Tambahkan fallback branch di client untuk unknown values.
  • Pertimbangkan field kategorisasi terpisah, misalnya payment_state dan status_detail.

3. Format tanggal berubah tanpa field baru

Awalnya tanggal dikirim sebagai waktu lokal tanpa timezone. Lalu provider mengganti ke ISO 8601 UTC. Sebagian client lama mem-parse dengan formatter tetap dan gagal; sebagian lain berhasil parse tetapi menampilkan waktu yang bergeser beberapa jam.

Mitigasi:

  • Jangan ganti format in-place jika ada banyak partner lama.
  • Tambahkan field baru untuk format baru.
  • Jelaskan timezone dan semantics secara eksplisit, bukan hanya format string.

Checklist desain kontrak agar versioning API tetap aman

  1. Tentukan apa yang dijamin stabil. Apakah semua field response stabil, atau hanya subset tertentu?
  2. Bedakan closed enum dan open enum. Jika enum bisa bertambah, tulis eksplisit di dokumentasi.
  3. Gunakan format tanggal yang jelas. Sebaiknya cantumkan timezone dan hindari format ambigu.
  4. Hindari perubahan in-place untuk field yang sudah dipakai luas.
  5. Sediakan masa transisi. Alias field, dual-write, atau dual-read jika perlu.
  6. Definisikan shape error yang konsisten. Banyak client bergantung pada kode dan struktur error.
  7. Jangan ubah default behavior diam-diam. Misalnya sorting, filtering, atau pagination.
  8. Tambahkan contract test. Bukan hanya unit test internal.
  9. Siapkan observabilitas per versi/client. Tanpa ini Anda tidak tahu siapa yang belum migrasi.
  10. Setel tanggal deprecation dan sunset. Jangan biarkan field legacy hidup tanpa batas.

Strategi rollout bertahap yang realistis

Versioning API yang aman hampir selalu memakai rollout bertahap, bukan switch besar sekaligus.

Tahap 1: tambahkan kontrak baru tanpa mencabut yang lama

Contoh: tambahkan created_at_iso, pertahankan created_at. Tambahkan customer, pertahankan customer_name.

Tahap 2: beri sinyal deprecation

Mulai kirim header Deprecation dan Sunset untuk endpoint/versi/field yang akan dihapus. Perbarui dokumentasi migrasi dan contoh response.

Tahap 3: ukur penggunaan client lama

Pantau siapa yang masih mengonsumsi versi atau field lama. Jika identitas partner tersedia, segmentasikan per partner. Jika tidak, gunakan kombinasi API key, user-agent, versi SDK, atau fingerprint lain yang aman dan tidak melanggar privasi internal organisasi Anda.

Tahap 4: lakukan canary atau allowlist migration

Aktifkan perilaku baru untuk sebagian partner atau traffic kecil lebih dulu. Pastikan ada mekanisme rollback cepat jika ditemukan parsing error, peningkatan 4xx/5xx, atau anomali bisnis.

Tahap 5: nonaktifkan secara terkontrol

Sebelum tanggal sunset, hubungi partner yang masih aktif di versi lama. Setelah penghentian, balikan error yang jelas dan terdokumentasi, bukan timeout atau response ambigu.

HTTP/1.1 410 Gone
Content-Type: application/json

{
  "error": {
    "code": "API_VERSION_SUNSET",
    "message": "Versi API ini sudah tidak didukung. Gunakan v2.",
    "migration_guide": "https://developer.example.com/docs/orders-v2-migration"
  }
}

Kenapa 410 berguna? Karena secara semantik ia menunjukkan resource/versi memang sudah tidak tersedia lagi, lebih jelas daripada gagal diam-diam atau tetap mengembalikan 404 yang membingungkan.

Observabilitas untuk mendeteksi client lama sebelum insiden terjadi

Tanpa observabilitas, deprecation hanyalah asumsi. Anda perlu tahu siapa yang memakai versi lama, field lama, dan fallback lama.

Metrik minimum yang sebaiknya ada

  • Jumlah request per versi API.
  • Jumlah request per partner atau API key.
  • Distribusi nilai header versi atau path versi.
  • Hit rate fallback logic.
  • Jumlah response yang masih mengandung field legacy.
  • Error parsing downstream yang terdeteksi dari callback, support ticket, atau partner monitoring.

Apa yang perlu dicatat di log

  • Versi API yang diminta.
  • Partner ID atau client ID jika tersedia.
  • Endpoint dan status code.
  • Apakah fallback diterapkan.
  • Feature flag atau rollout bucket yang aktif.
  • Correlation ID untuk investigasi lintas layanan.

Jika Anda menjalankan API gateway, sebagian observabilitas ini bisa diletakkan di gateway agar tidak semua service harus mengimplementasikan ulang. Namun keputusan apakah field legacy masih dikirim biasanya tetap perlu log dari service aplikasi.

Sinyal bahaya saat migrasi

  • Lonjakan 4xx setelah rollout versi baru.
  • Request ulang berulang dari partner yang sama.
  • Peningkatan support ticket soal data kosong atau status tak dikenal.
  • Gap metrik bisnis, misalnya order sukses turun padahal traffic tetap.

Masalah kompatibilitas sering muncul sebagai anomali bisnis, bukan error teknis yang eksplisit. Karena itu dashboard API sebaiknya dibaca bersama metrik domain.

Contract testing dan validasi kompatibilitas

Unit test internal tidak cukup untuk memastikan perubahan aman bagi client. Anda butuh tes yang memverifikasi kontrak antar sistem.

Apa yang perlu diuji

  • Response versi lama tetap memiliki field wajib lama.
  • Field baru tidak mengubah arti field lama.
  • Enum baru tidak bocor ke client lama jika ada mapping fallback.
  • Format tanggal per versi tetap konsisten.
  • Shape error untuk versi lama dan baru sesuai dokumentasi.

Praktik yang membantu

  • Simpan contoh payload yang disetujui sebagai fixture regression.
  • Tambahkan test untuk unknown enum di SDK/client resmi.
  • Jalankan compatibility test di pipeline sebelum rilis.
  • Jika ada consumer-driven contract testing, gunakan untuk partner internal atau integrasi kritis.

Kesalahan umum adalah hanya mengetes skenario sukses. Padahal perubahan error code dan validasi request juga sering mematahkan automasi partner.

Langkah migrasi aman untuk provider API

  1. Inventarisasi kontrak yang dipakai nyata di produksi. Jangan hanya percaya dokumentasi; cek log dan payload aktual.
  2. Klasifikasikan perubahan. Tentukan mana non-breaking, mana breaking, mana butuh fallback.
  3. Tambahkan kontrak baru secara paralel. Hindari mengganti field lama secara langsung.
  4. Rilis dokumentasi migrasi yang spesifik. Tunjukkan before/after payload dan dampaknya ke client.
  5. Aktifkan deprecation dan sunset signal.
  6. Monitor penggunaan versi lama.
  7. Lakukan rollout bertahap.
  8. Hubungi partner yang belum pindah.
  9. Matikan versi lama dengan error yang jelas.
  10. Hapus fallback code setelah benar-benar tidak dipakai.

Rekomendasi praktis yang paling sering berhasil

  • Gunakan path versioning jika ekosistem client Anda beragam dan observabilitas operasional menjadi prioritas.
  • Anggap perubahan enum, format tanggal, dan default behavior sebagai area berisiko tinggi meski schema terlihat sederhana.
  • Tambahkan field baru lebih dulu, jangan ganti field lama di tempat yang sama.
  • Gunakan Sunset header dan dokumentasi migrasi yang konkret, bukan pengumuman umum.
  • Bangun fallback dengan batas waktu, metrik, dan test yang jelas.
  • Ukur trafik client lama sebelum mematikan versi lama; jangan menebak.

Pada akhirnya, versioning API tanpa merusak client bukan soal memilih /v1 atau header versi semata. Kuncinya adalah menjaga contract, memahami apa yang benar-benar breaking bagi konsumen, memberi masa transisi yang terukur, dan menyiapkan observabilitas yang cukup untuk mengambil keputusan penghentian dengan aman.