Desain API untuk klien lama bukan sekadar menjaga endpoint lama tetap hidup. Tantangan utamanya adalah memastikan perubahan di sisi server tidak merusak integrasi yang sudah berjalan, terutama ketika klien berada di lingkungan terbatas, sulit diperbarui, memakai library HTTP lama, atau memiliki perilaku retry yang tidak konsisten.

Prinsip dasarnya sederhana: kontrak API harus stabil, perubahan harus aditif, operasi harus aman saat diulang, dan kegagalan jaringan harus dianggap normal. Dengan pendekatan ini, API tetap dapat berevolusi tanpa membuat integrasi menjadi rapuh. Artikel ini membahas pola yang praktis untuk mencapainya pada API HTTP.

Mengapa kompatibilitas API perlu diperlakukan seperti kontrak

Dalam sistem nyata, masalah integrasi jarang muncul karena logika bisnis murni. Yang lebih sering terjadi justru hal-hal seperti:

  • server menambah field baru tetapi klien lama gagal karena parser terlalu ketat,
  • server mengubah bentuk error dan klien tidak lagi bisa membaca penyebab kegagalan,
  • permintaan POST diulang akibat timeout sehingga data tercatat ganda,
  • webhook sampai dua kali atau tidak berurutan, lalu konsumen memprosesnya secara salah.

API yang stabil harus dirancang dengan asumsi bahwa jaringan tidak andal, klien tidak selalu modern, dan perubahan server tidak langsung diikuti pembaruan klien. Analogi kompatibilitas sistem lama berguna di sini: jika sebuah lingkungan lama tetap harus bisa bekerja, maka perubahan harus mengutamakan backward compatibility, bukan hanya kebersihan desain internal.

Prinsip utama desain API untuk klien lama

1. Anggap kontrak wire format sebagai hal yang sakral

Yang dimaksud kontrak bukan hanya path dan method, tetapi juga:

  • nama field JSON,
  • tipe data,
  • status code,
  • struktur error,
  • header penting,
  • semantik retry dan idempotensi.

Perubahan internal boleh besar, tetapi representasi yang dilihat klien harus berubah secara hati-hati.

2. Utamakan perubahan aditif

Perubahan yang relatif aman untuk klien lama biasanya bersifat aditif, misalnya:

  • menambah field respons baru tanpa menghapus field lama,
  • menambah endpoint baru alih-alih mengubah semantik endpoint lama,
  • menambah nilai enum baru hanya jika klien memang didesain untuk mengabaikan nilai yang tidak dikenal.

Perubahan yang berisiko tinggi:

  • mengganti nama field,
  • mengubah tipe dari string ke object,
  • mengubah arti status code,
  • menghapus field yang tampak tidak dipakai,
  • mengganti default behavior tanpa mekanisme opt-in.

3. Desain untuk kegagalan parsial

Dalam jaringan nyata, kegagalan bukan biner. Server bisa sudah memproses permintaan tetapi respons tidak pernah sampai ke klien. Dari sudut pandang klien, kondisi itu terlihat seperti timeout. Jika operasi tidak idempoten, retry dapat menciptakan duplikasi.

Karena itu, desain API harus menjelaskan dengan tegas:

  • request mana yang aman untuk diulang,
  • header apa yang diperlukan untuk deduplikasi,
  • status code apa yang menandakan klien boleh retry,
  • berapa lama hasil deduplikasi disimpan.

Versioning yang realistis tanpa memecah klien lama

Kapan versioning diperlukan

Jika perubahan masih bisa dibuat secara aditif, sering kali Anda belum perlu membuat versi baru. Versi baru dibutuhkan saat kontrak lama tidak lagi bisa dipertahankan tanpa ambiguitas, misalnya:

  • struktur respons berubah secara mendasar,
  • aturan validasi lama harus diganti total,
  • makna suatu field berubah, bukan sekadar nilainya bertambah.

Pilih strategi versioning yang sederhana

Untuk kompatibilitas klien lama, strategi yang paling mudah dioperasikan biasanya adalah versi di path, misalnya /v1/orders dan /v2/orders. Alasan praktisnya:

  • mudah di-debug dengan log dan proxy,
  • mudah diuji manual,
  • tidak bergantung pada parsing header khusus di klien lama,
  • lebih jelas saat beberapa versi hidup bersamaan.

Versioning via header juga bisa dipakai, tetapi lebih mudah memunculkan inkonsistensi jika sebagian komponen perantara tidak meneruskan header dengan benar.

Kebijakan perubahan yang aman

Pada satu versi mayor, usahakan aturan berikut:

  • field yang sudah ada jangan dihapus,
  • jangan ubah tipe field,
  • jangan ubah status code sukses menjadi pola lain tanpa alasan kuat,
  • tambahkan field baru sebagai opsional,
  • jangan mengharuskan klien mengirim field baru pada endpoint lama.

Jika Anda perlu menandai field lama sebagai usang, lakukan deprecation dengan dokumentasi dan periode transisi yang jelas. Jangan menghapus diam-diam hanya karena telemetri menunjukkan “hampir tidak ada yang pakai”. Klien lama sering tidak terlihat sempurna di observability modern.

Contoh kontrak API yang backward-compatible

Contoh request membuat order dengan idempotency key

POST /v1/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
Idempotency-Key: ord-create-7f3c1d5a-20260609
X-Request-Id: req-9c2b7f0d

{
  "customer_id": "cust_12345",
  "amount": 150000,
  "currency": "IDR",
  "reference": "INV-2026-0001"
}

Contoh respons sukses yang aman untuk berevolusi

HTTP/1.1 201 Created
Content-Type: application/json
X-Request-Id: req-9c2b7f0d
Location: /v1/orders/ord_8a12bc34

{
  "id": "ord_8a12bc34",
  "status": "created",
  "customer_id": "cust_12345",
  "amount": 150000,
  "currency": "IDR",
  "reference": "INV-2026-0001",
  "created_at": "2026-06-09T10:15:30Z"
}

Di masa depan, server bisa menambah field seperti metadata atau links tanpa merusak klien yang mengabaikan field tak dikenal:

{
  "id": "ord_8a12bc34",
  "status": "created",
  "customer_id": "cust_12345",
  "amount": 150000,
  "currency": "IDR",
  "reference": "INV-2026-0001",
  "created_at": "2026-06-09T10:15:30Z",
  "metadata": {
    "channel": "partner"
  }
}

Yang harus dihindari adalah mengganti customer_id menjadi object seperti ini pada versi yang sama:

{
  "customer": {
    "id": "cust_12345"
  }
}

Secara internal itu mungkin lebih rapi, tetapi bagi klien lama ini adalah perubahan yang merusak.

Skema error harus stabil dan bisa ditangani mesin

Banyak integrasi rapuh karena respons error berubah-ubah. Kadang hanya string, kadang HTML, kadang object dengan field berbeda. Untuk klien lama dan automasi, gunakan skema error yang stabil.

Contoh skema error yang konsisten

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
X-Request-Id: req-4fd219ac

{
  "error": {
    "code": "validation_error",
    "message": "Payload tidak valid",
    "details": [
      {
        "field": "currency",
        "reason": "unsupported_value"
      }
    ],
    "retryable": false
  }
}

Beberapa rekomendasi praktis:

  • code harus stabil dan cocok dipakai logika program.
  • message boleh ditujukan untuk manusia, tetapi jangan dijadikan satu-satunya dasar pengambilan keputusan.
  • retryable membantu klien sederhana menentukan apakah aman mencoba lagi.
  • X-Request-Id penting untuk korelasi log saat debugging.

Pemetaan status code yang masuk akal

  • 400: payload salah format atau request tidak valid secara umum.
  • 401/403: autentikasi atau otorisasi gagal.
  • 404: resource tidak ditemukan.
  • 409: konflik state atau idempotency key bentrok dengan payload berbeda.
  • 422: validasi bisnis gagal.
  • 429: rate limit, klien boleh retry sesuai petunjuk.
  • 500/502/503/504: gangguan server atau jaringan, umumnya kandidat retry.

Kesalahan umum adalah selalu mengembalikan 200 dengan field success: false. Ini menyulitkan proxy, observability, retry policy, dan client library standar.

Timeout dan retry: anggap jaringan tidak andal

Atur timeout dengan eksplisit

Jangan mengandalkan default library HTTP karena nilainya bisa tidak jelas atau terlalu lama. Dari perspektif integrasi, timeout yang eksplisit lebih aman daripada menunggu tanpa batas.

Minimal, pikirkan tiga lapisan timeout:

  • connect timeout: waktu membuka koneksi,
  • read/response timeout: waktu menunggu respons,
  • server-side processing timeout: batas internal agar request tidak menggantung terlalu lama.

Angkanya bergantung pada jenis operasi, tetapi pola umumnya: operasi sinkron yang sering dipanggil harus cepat dan memiliki batas yang jelas. Jika proses bisnis memang lama, pertimbangkan pola asinkron seperti menerima request, mengembalikan 202 Accepted, lalu sediakan endpoint status.

Retry dengan backoff dan jitter

Retry diperlukan untuk kegagalan sementara, tetapi retry naif bisa memperburuk keadaan. Jika semua klien langsung mengulang bersamaan, server yang sedang tertekan akan makin sibuk. Karena itu gunakan exponential backoff dan, idealnya, jitter.

Contoh kebijakan sederhana:

  • retry hanya untuk 429, 502, 503, 504, atau timeout jaringan,
  • jangan retry untuk 400, 401, 403, 404, 422,
  • batasi jumlah retry,
  • gunakan backoff bertahap,
  • hormati header Retry-After jika tersedia.
Percobaan 1: langsung
Percobaan 2: tunggu ~1 detik
Percobaan 3: tunggu ~2 detik
Percobaan 4: tunggu ~4 detik
Tambahkan jitter kecil agar tidak serempak

Jika klien lama tidak mendukung logika retry canggih, dokumentasikan aturan minimal: endpoint mana yang aman untuk diulang dan dalam kondisi apa.

Kapan retry justru berbahaya

Retry berbahaya pada operasi yang membuat efek samping tanpa mekanisme deduplikasi, seperti membuat order, menagih pembayaran, atau mengirim instruksi sekali jalan. Di sinilah idempotency key menjadi penting.

Idempotensi untuk POST: wajib jika efek sampingnya penting

Apa masalah yang diselesaikan

Bayangkan klien mengirim POST /v1/orders. Server sebenarnya berhasil membuat order, tetapi koneksi putus sebelum respons diterima. Klien melihat timeout lalu mencoba lagi. Tanpa idempotensi, dua order bisa tercipta.

Cara kerja idempotency key

Klien mengirim header unik, misalnya Idempotency-Key, untuk satu niat operasi. Server menyimpan hasil pertama yang berhasil diproses untuk kombinasi tertentu, lalu mengembalikan hasil yang sama jika request identik dikirim ulang.

Aturan implementasi yang aman:

  • kunci harus unik per niat operasi, bukan per koneksi,
  • server perlu menyimpan fingerprint request penting agar key yang sama dengan payload berbeda dianggap konflik,
  • hasil deduplikasi perlu memiliki masa simpan yang jelas,
  • respons retry sebaiknya konsisten dengan respons pertama, termasuk status code yang relevan.

Contoh perilaku yang diharapkan

Request pertama:

POST /v1/orders
Idempotency-Key: ord-create-7f3c1d5a-20260609

Respons:

HTTP/1.1 201 Created
{
  "id": "ord_8a12bc34",
  "status": "created"
}

Jika request yang sama dikirim ulang karena timeout jaringan, server dapat mengembalikan hasil yang sama berdasarkan key yang sudah tersimpan:

HTTP/1.1 201 Created
{
  "id": "ord_8a12bc34",
  "status": "created"
}

Namun jika klien mengirim key yang sama dengan payload berbeda, server sebaiknya menolak agar tidak terjadi ambiguitas:

HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "error": {
    "code": "idempotency_conflict",
    "message": "Idempotency-Key sudah digunakan untuk payload berbeda",
    "retryable": false
  }
}

Catatan implementasi server

Implementasi idempotensi yang sering dipakai:

  1. cek apakah Idempotency-Key sudah ada,
  2. jika belum ada, simpan entri “sedang diproses” secara atomik,
  3. proses operasi bisnis,
  4. simpan hasil final yang akan dikembalikan saat retry,
  5. jika request kedua datang saat proses pertama belum selesai, kembalikan status yang jelas, misalnya konflik sementara atau respons yang menyuruh klien mencoba lagi nanti.

Kuncinya adalah operasi penyimpanan key harus atomik. Jika tidak, dua request paralel dengan key yang sama bisa lolos bersamaan.

Webhooks: selalu anggap bisa ganda dan tidak berurutan

Webhook sering dianggap seperti callback sinkron, padahal kenyataannya lebih mirip pengiriman event di jaringan tidak andal. Konsumen webhook harus siap menghadapi:

  • pengiriman ganda,
  • urutan tidak terjamin,
  • keterlambatan,
  • percobaan ulang dari pengirim.

Kontrak webhook yang lebih tahan banting

POST /partner/webhooks/order HTTP/1.1
Content-Type: application/json
X-Event-Id: evt_01JXZ7R2M9
X-Event-Type: order.updated
X-Event-Time: 2026-06-09T10:20:00Z
X-Signature: sha256=...

{
  "id": "evt_01JXZ7R2M9",
  "type": "order.updated",
  "occurred_at": "2026-06-09T10:20:00Z",
  "data": {
    "order_id": "ord_8a12bc34",
    "status": "paid",
    "version": 3
  }
}

Header dan field penting:

  • X-Event-Id: untuk deduplikasi di sisi penerima.
  • X-Event-Type: routing handler.
  • X-Event-Time atau occurred_at: membantu observability, tetapi jangan dijadikan satu-satunya dasar urutan.
  • version pada resource atau event: membantu mendeteksi event lama yang datang belakangan.
  • X-Signature: verifikasi integritas dan autentikasi pengirim.

Cara aman memproses webhook

  1. verifikasi signature,
  2. cek apakah event_id pernah diproses,
  3. simpan jejak penerimaan secara atomik,
  4. proses event secara idempoten,
  5. jika ada nomor versi resource, abaikan event yang lebih lama dari state saat ini.

Contoh: event order.updated version=2 datang setelah version=3 sudah diproses. Jangan menurunkan state order ke versi lama hanya karena event lama baru tiba belakangan.

Catatan: jika urutan benar-benar penting, webhook sebaiknya hanya menjadi sinyal. Konsumen kemudian mengambil state terbaru dari API sumber melalui GET resource. Ini lebih aman daripada menganggap payload webhook selalu merupakan sumber kebenaran final.

Skenario kegagalan integrasi yang sering terjadi

1. Timeout setelah server sukses memproses

Gejala: klien melihat timeout, lalu mengirim ulang POST dan tercipta data ganda.

Pencegahan: gunakan Idempotency-Key, simpan hasil pertama, dan dokumentasikan bahwa klien harus mempertahankan key yang sama saat retry.

2. Parser klien gagal setelah server menambah field baru

Gejala: integrasi lama rusak walau perubahan dianggap kecil.

Pencegahan: edukasi konsumen agar mengabaikan field tak dikenal; di sisi server hindari perubahan non-aditif pada field lama.

3. Klien retry pada error yang sebenarnya permanen

Gejala: banjir request untuk 422 atau 401.

Pencegahan: status code dan field retryable harus jelas; dokumentasikan kapan retry diperbolehkan.

4. Webhook diproses dua kali

Gejala: invoice terkirim dua kali atau status berubah bolak-balik.

Pencegahan: simpan event_id, buat handler idempoten, dan gunakan nomor versi resource jika ada.

5. Perubahan kecil pada error schema mematahkan monitoring

Gejala: dashboard atau alert yang membaca error.code berhenti berfungsi.

Pencegahan: perlakukan error schema sebagai kontrak publik; jangan menghapus field inti tanpa versi baru.

Header penting yang sebaiknya dipertimbangkan

  • Accept: application/json — memperjelas format respons.
  • Content-Type: application/json — wajib untuk payload JSON.
  • X-Request-Id — korelasi log end-to-end.
  • Idempotency-Key — deduplikasi operasi POST yang berefek samping.
  • Retry-After — petunjuk kapan klien boleh mencoba lagi, terutama pada 429 atau 503.
  • Location — berguna setelah 201 Created.
  • X-Event-Id dan X-Signature — penting untuk webhook.

Anda tidak harus memakai semua header di atas, tetapi pilih yang mendukung kontrak integrasi secara eksplisit. Tujuannya adalah mengurangi asumsi tersembunyi di sisi klien.

Anti-pattern yang membuat integrasi rapuh

  • Mengubah field lama diam-diam hanya karena “semua klien internal sudah siap”. Biasanya ada konsumen yang tertinggal.
  • Mengembalikan HTML saat error pada endpoint JSON. Ini menyulitkan parser dan debugging otomatis.
  • Selalu 200 OK untuk semua hasil. Ini merusak semantik HTTP dan retry policy.
  • Membuat retry otomatis tanpa idempotensi untuk operasi pembuatan data.
  • Mengandalkan urutan webhook seolah-olah dijamin jaringan.
  • Menjadikan message error sebagai kontrak alih-alih error.code yang stabil.
  • Menambah field wajib pada endpoint lama tanpa default atau mekanisme negosiasi.
  • Menetapkan timeout terlalu panjang atau tak terbatas sehingga koneksi menggantung dan kapasitas habis.

Checklist review API sebelum rilis

  1. Apakah perubahan ini aditif? Jika tidak, apakah perlu versi baru?
  2. Apakah field lama tetap ada dengan tipe dan makna yang sama?
  3. Apakah klien lama masih bisa mem-parsing respons jika field baru ditambahkan?
  4. Apakah schema error konsisten di semua endpoint?
  5. Apakah status code sudah sesuai semantik HTTP?
  6. Apakah ada X-Request-Id atau mekanisme korelasi serupa?
  7. Apakah operasi POST yang berefek samping mendukung Idempotency-Key?
  8. Apakah konflik idempotensi untuk payload berbeda ditangani jelas?
  9. Apakah dokumentasi retry menjelaskan error mana yang boleh diulang?
  10. Apakah server mengirim Retry-After saat relevan?
  11. Apakah webhook punya event ID unik, signature, dan strategi deduplikasi?
  12. Apakah konsumen webhook diarahkan untuk siap menerima event ganda atau out-of-order?
  13. Apakah timeout client dan server sudah didefinisikan eksplisit?
  14. Apakah observability cukup untuk membedakan timeout jaringan, validasi gagal, dan gangguan server?

Tips debugging saat integrasi mulai rapuh

  • Catat X-Request-Id di klien dan server agar satu kasus bisa ditelusuri dari dua sisi.
  • Log header penting seperti Idempotency-Key, tetapi hindari mencatat data sensitif mentah.
  • Jika terjadi duplikasi, cek apakah key idempotensi berubah saat retry.
  • Jika webhook tampak aneh, periksa apakah event lama datang belakangan dan menimpa state baru.
  • Jika klien sering timeout, bedakan apakah masalah terjadi saat koneksi, saat menunggu respons, atau setelah server memulai proses.
  • Uji dengan fault injection sederhana: putuskan koneksi setelah request diterima, kirim ulang request yang sama, dan pastikan hasil tetap satu.

Penutup

Desain API untuk klien lama menuntut disiplin pada kontrak, bukan sekadar kompatibilitas sesaat. Versioning harus dipakai seperlunya, perubahan sebaiknya aditif, skema error harus stabil, dan semua operasi penting perlu dirancang dengan asumsi bahwa timeout serta retry akan terjadi.

Jika Anda hanya mengambil beberapa aturan inti dari artikel ini, ambillah yang berikut: jangan merusak kontrak lama, buat retry aman dengan idempotensi, dan perlakukan webhook sebagai event yang bisa ganda maupun tidak berurutan. Tiga hal itu saja sudah mengurangi sebagian besar integrasi rapuh yang sering muncul pada API HTTP di sistem nyata.