Ketika biaya membuat ulang aplikasi turun karena bantuan AI, satu hal tetap mahal: menjaga kompatibilitas kontrak API. Kode server bisa ditulis ulang lebih cepat, tetapi klien mobile yang belum update, partner integrasi yang jarang deploy, webhook consumer pihak ketiga, dan proses retry di jaringan yang tidak stabil tetap memaksa Anda mempertahankan perilaku lama.
Itulah mengapa kontrak API anti-rewrite penting. Tujuannya bukan membuat API yang tidak pernah berubah, melainkan membuat perubahan menjadi terukur: field jelas, error konsisten, autentikasi bisa diputar tanpa memutus klien, operasi sensitif aman di-retry, dan webhook punya semantik pengiriman yang bisa diandalkan. Artikel ini membahas pola desain praktis untuk mencapainya, termasuk anti-pattern yang sering membuat rewrite semakin mahal.
Mengapa kontrak API yang buruk membuat rewrite makin mahal
Rewrite backend sering dianggap mudah dibanding migrasi kontrak. Kenyataannya, biaya terbesar biasanya bukan menulis ulang business logic, tetapi mempertahankan asumsi yang sudah bocor ke luar sistem.
- Klien mengandalkan perilaku implisit, misalnya field kadang null, kadang hilang, atau urutan array dianggap bermakna padahal tidak didokumentasikan.
- Error tidak konsisten, sehingga klien harus menebak apakah 400, 409, atau 422 perlu di-retry, ditampilkan ke user, atau dianggap bug.
- POST tidak idempoten, sehingga retry akibat timeout bisa membuat order ganda, pembayaran ganda, atau provisioning ganda.
- Webhook dianggap exactly-once, padahal pada praktiknya yang realistis adalah at least once delivery. Tanpa deduplikasi, sistem penerima akan rusak saat ada pengiriman ulang.
- Token auth tidak punya strategi rotasi, sehingga perubahan secret, key leak, atau migrasi skema auth berubah menjadi insiden besar.
Jika kontrak semacam ini sudah dipakai banyak klien, rewrite tidak benar-benar menghapus kompleksitas. Ia hanya memindahkannya ke layer adapter, compatibility shim, dan cabang logika khusus untuk perilaku lama.
Prinsip praktis: lebih murah mendesain kontrak eksplisit sejak awal daripada menebak kompatibilitas dari perilaku historis.
Prinsip desain kontrak API yang stabil
1. Versioning seperlunya, bukan reflex bawaan
Versioning berguna, tetapi terlalu cepat menambah versi sering menutupi desain yang lemah. Tidak semua perubahan butuh versi baru. Tambah field opsional biasanya bisa kompatibel. Mengganti arti field lama, menghapus field, atau mengubah semantik error biasanya tidak bisa.
Pendekatan yang umum dan cukup aman:
- Versioning di path seperti
/v1/paymentsjika Anda butuh batas kontrak yang tegas dan mudah diobservasi. - Minimalkan jumlah versi aktif. Dua versi aktif biasanya jauh lebih sehat daripada memelihara banyak varian perilaku.
- Tetapkan kebijakan sunset: dokumentasikan kapan versi lama deprecated, bagaimana notifikasi diberikan, dan berapa lama masa transisi.
Hal yang lebih penting daripada mekanisme versi adalah aturan kompatibilitas. Contoh aturan yang sehat:
- Menambah field response baru diperbolehkan.
- Menghapus atau mengganti tipe field lama adalah breaking change.
- Mengubah perilaku retry, auth, atau status code untuk kasus lama adalah breaking change meski payload tampak sama.
2. Skema request/response harus eksplisit
Kontrak yang tahan lama selalu mengurangi area abu-abu. Jangan biarkan klien menebak field wajib, format timestamp, mata uang, enum, atau status transisi.
Contoh request yang eksplisit untuk operasi sensitif:
POST /v1/payments HTTP/1.1
Authorization: Bearer eyJ...
Content-Type: application/json
Idempotency-Key: 2f7d8b65-4a52-4d8d-9e9f-7b8f2f0f4a11
X-Request-Id: c5f3e0d1-8bb8-4d7c-a3b1-2d9a7a6e1a77
{
"customer_id": "cust_12345",
"amount": {
"value": 150000,
"currency": "IDR"
},
"payment_method": "bank_transfer",
"reference": "INV-2026-00123",
"metadata": {
"source": "checkout-web"
}
}Contoh response sukses:
HTTP/1.1 201 Created
Content-Type: application/json
X-Request-Id: c5f3e0d1-8bb8-4d7c-a3b1-2d9a7a6e1a77
{
"id": "pay_8a1c7d",
"status": "pending",
"customer_id": "cust_12345",
"amount": {
"value": 150000,
"currency": "IDR"
},
"payment_method": "bank_transfer",
"reference": "INV-2026-00123",
"created_at": "2026-07-11T10:15:30Z"
}Beberapa aturan praktis:
- Gunakan tipe yang konsisten. Jika
amount.valueinteger dalam satuan terkecil, jangan di endpoint lain berubah menjadi string atau decimal bebas. - Bedakan field opsional, nullable, dan absent. Ini tiga hal yang berbeda.
- Jangan overload satu field untuk banyak arti. Misalnya
status=faileddipakai sekaligus untuk validasi gagal, ditolak bank, dan timeout upstream. - Gunakan nama yang stabil secara domain, bukan nama yang mencerminkan detail implementasi internal.
3. Error model harus konsisten dan bisa diotomasi
Klien tidak hanya butuh pesan error untuk manusia. Mereka butuh kode error yang stabil untuk keputusan otomatis: retry, minta input ulang, atau eskalasi manual.
Contoh error response:
HTTP/1.1 409 Conflict
Content-Type: application/json
X-Request-Id: c5f3e0d1-8bb8-4d7c-a3b1-2d9a7a6e1a77
{
"error": {
"code": "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD",
"message": "Idempotency-Key sudah pernah dipakai dengan payload berbeda.",
"retryable": false,
"details": {
"idempotency_key": "2f7d8b65-4a52-4d8d-9e9f-7b8f2f0f4a11"
}
}
}Prinsip yang membantu:
- Bedakan status HTTP dan kode error domain. HTTP memberi kategori umum, kode error memberi makna bisnis atau kontrak.
- Sediakan flag atau dokumentasi retryability. Klien tidak boleh menebak dari string message.
- Selalu kirim request ID agar debugging lintas sistem lebih mudah.
Pemetaan yang sering berguna:
400untuk request malformed.401untuk auth gagal atau token tidak valid.403untuk kredensial valid tetapi tidak berhak.404untuk resource tidak ditemukan jika aman diungkapkan.409untuk konflik state atau konflik idempotensi.422untuk validasi semantik request.429untuk rate limit.5xxuntuk kegagalan server atau dependency.
Trade-off-nya: model error yang terlalu rinci bisa membuat API sulit diubah. Karena itu, stabilkan kode error inti terlebih dulu, lalu letakkan detail tambahan di details yang lebih fleksibel.
Idempotensi dan retry yang aman untuk operasi sensitif
Mengapa POST sensitif perlu Idempotency-Key
Secara teori, POST tidak idempoten. Secara operasional, klien akan tetap melakukan retry karena timeout, koneksi putus, load balancer reset, atau respons hilang setelah server sebenarnya berhasil memproses. Tanpa proteksi, satu aksi user bisa berubah menjadi banyak efek samping.
Untuk operasi seperti pembayaran, pembuatan order, pengiriman email penting, atau provisioning resource berbiaya, gunakan Idempotency-Key unik yang dikirim klien per niat operasi, bukan per percobaan HTTP.
Perilaku server yang disarankan:
- Terima
Idempotency-Keypada endpoint sensitif. - Simpan asosiasi antara key, fingerprint payload, status pemrosesan, dan response final.
- Jika request identik datang lagi dengan key yang sama, kembalikan hasil yang sama.
- Jika key sama tetapi payload berbeda, kembalikan konflik, biasanya
409.
Contoh alur penyimpanan server secara konseptual:
key = headers["Idempotency-Key"]
fingerprint = hash(normalized_request_body)
existing = idempotency_store.get(key)
if existing == null:
create record { key, fingerprint, status: "in_progress" }
result = process_business_operation()
save record { key, fingerprint, status: "completed", response: result }
return result
if existing.fingerprint != fingerprint:
return 409 conflict
if existing.status == "completed":
return existing.response
if existing.status == "in_progress":
return 409 or 202 depending on API semanticsCatatan penting:
- Fingerprint payload perlu dinormalisasi. Jika Anda hash JSON mentah, perbedaan urutan key bisa dianggap request berbeda padahal semantiknya sama.
- Tentukan masa simpan key sesuai risiko bisnis. Terlalu singkat bisa membuka celah duplikasi, terlalu lama membebani storage.
- Idempotensi tidak menggantikan transaksi. Anda tetap butuh proteksi konsistensi di database dan dependency upstream.
Strategi retry yang aman
Retry yang benar bukan berarti “ulang semua kegagalan”. Ia butuh aturan.
- Aman di-retry: timeout jaringan, koneksi terputus, 502/503/504, atau 429 dengan kebijakan backoff yang jelas.
- Tidak otomatis di-retry: 400, 401, 403, 404, 422, dan 409 tertentu yang menandakan request perlu diperbaiki.
Rekomendasi praktis:
- Gunakan exponential backoff dengan jitter agar tidak memicu retry storm.
- Hormati header seperti
Retry-Afterjika disediakan. - Batasi jumlah retry dan total deadline permintaan.
- Pastikan observability: log key, request ID, dan hasil retry.
Contoh pseudo-config klien:
retry_on = [429, 502, 503, 504, network_timeout]
max_attempts = 4
backoff = exponential_with_jitter
require_idempotency_key_for = [POST /v1/payments, POST /v1/orders]Kesalahan umum adalah mengaktifkan retry global untuk semua POST tanpa idempotensi. Itu bukan reliability; itu mesin pengganda bug.
Webhook: delivery semantics, deduplication, dan kompatibilitas
Asumsi yang benar: webhook hampir selalu at-least-once
Webhook yang sehat harus dirancang dengan asumsi bahwa event bisa datang lebih dari sekali, datang terlambat, dan kadang tidak berurutan. Jangan mendesain consumer dengan asumsi exactly-once atau ordering global kecuali Anda benar-benar mengontrol kanal dan storage ujung ke ujung.
Contoh webhook event:
POST /partner/webhooks/payments HTTP/1.1
Content-Type: application/json
X-Event-Id: evt_01J2ABCDEF
X-Event-Type: payment.updated
X-Event-Timestamp: 2026-07-11T10:20:01Z
X-Signature: t=1720693201,v1=ab34f...
{
"id": "evt_01J2ABCDEF",
"type": "payment.updated",
"created_at": "2026-07-11T10:20:01Z",
"data": {
"payment_id": "pay_8a1c7d",
"status": "settled",
"amount": {
"value": 150000,
"currency": "IDR"
}
}
}Prinsip pengiriman webhook yang tahan lama:
- Setiap event punya ID unik global.
- Payload event immutable setelah dikirim; jika ada perubahan state baru, kirim event baru.
- Consumer mengakui penerimaan secepat mungkin, lalu proses async di belakang jika perlu.
- Provider melakukan retry pada kegagalan sementara, dengan backoff.
Deduplikasi di sisi penerima
Karena delivery bisa berulang, consumer harus menyimpan event_id atau kombinasi kunci deduplikasi lain. Jika event yang sama datang lagi, kembalikan sukses tanpa memproses ulang efek samping.
if dedup_store.contains(event.id):
return 200
begin transaction
insert dedup record(event.id)
apply side effects if not yet applied
commit
return 200Detail penting:
- Simpan dedup record sebelum atau atomik dengan side effect. Jika side effect terjadi tetapi dedup record gagal tersimpan, event ulang bisa memicu duplikasi.
- Jika event out of order mungkin terjadi, desain state machine Anda agar tahan terhadap transisi yang datang terlambat, misalnya dengan membandingkan versi resource atau timestamp domain yang dipercaya.
Signature dan rotasi secret untuk webhook
Webhook tanpa verifikasi signature pada dasarnya mengandalkan IP allowlist atau kepercayaan jaringan, yang sering tidak cukup. Gunakan signature berbasis secret bersama, sertakan timestamp untuk mencegah replay, dan dukung rotasi secret dengan masa overlap.
Pola yang umum:
- Provider menandatangani string kanonik yang mencakup timestamp dan body mentah.
- Consumer memverifikasi terhadap current secret dan next secret selama masa rotasi.
- Setelah semua consumer berpindah, secret lama dicabut.
Jangan melakukan canonicalization body yang ambigu di sisi yang berbeda. Untuk signature, biasanya lebih aman memakai raw request body apa adanya.
Auth token rotation tanpa memutus klien lama
Kontrak auth sering menjadi bagian paling rapuh saat rewrite. Token, scope, header, dan umur kredensial yang berubah tiba-tiba dapat memutus integrasi yang jarang dipelihara.
Pola rotasi yang aman
- Dukung overlap kredensial: token atau key lama dan baru valid bersamaan untuk jangka waktu tertentu.
- Gunakan key identifier seperti
kidatau ID credential agar server tahu material verifikasi mana yang dipakai. - Pisahkan kegagalan auth dan authorization. Klien perlu tahu apakah token invalid, expired, atau kurang scope.
- Dokumentasikan umur token, mekanisme refresh, dan kapan refresh harus dilakukan.
Contoh respons auth gagal yang lebih operasional:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="token expired"
Content-Type: application/json
{
"error": {
"code": "TOKEN_EXPIRED",
"message": "Access token telah kedaluwarsa.",
"retryable": false
}
}Jika Anda memigrasikan skema auth saat rewrite, hindari memaksa semua klien pindah pada hari yang sama. Lebih aman menyediakan compatibility window, observability per credential, dan dashboard adopsi agar Anda tahu siapa yang belum bermigrasi.
Masalah umum saat klien lama masih aktif
- Header auth berbeda antara SDK lama dan baru.
- Clock skew membuat token dianggap expired terlalu cepat.
- Scope baru ditambahkan tetapi klien lama tidak pernah meminta scope itu.
- Refresh token flow berubah, tetapi mobile app lama tidak bisa dipaksa update cepat.
Pada fase ini, telemetry lebih penting daripada asumsi. Catat metrik berdasarkan versi klien, credential ID, endpoint, dan kode error auth.
Edge case integrasi saat client lama masih aktif
Bagian tersulit dari kontrak API anti-rewrite justru terjadi saat sistem baru hidup berdampingan dengan klien lama.
Tambahan field vs perubahan makna
Menambah field response umumnya aman, tetapi mengubah makna field lama sangat berbahaya. Misalnya field status=pending sebelumnya berarti “menunggu pembayaran”, lalu setelah rewrite juga mencakup “sedang diverifikasi manual”. Banyak klien lama akan salah mengambil keputusan.
Jika makna bisnis berubah, lebih aman:
- tambahkan field baru yang lebih spesifik, atau
- buat versi baru jika perubahan semantik tidak bisa dijelaskan tanpa merusak asumsi lama.
Null, field hilang, dan default implisit
Klien lama sering bergantung pada default yang tidak pernah ditulis. Contoh:
- Field kosong dianggap
false. - Field yang hilang dianggap sama dengan string kosong.
- Nilai enum baru membuat parser lama gagal.
Karena itu, sebelum merilis perubahan:
- uji dengan payload nyata dari klien lama,
- hindari enum baru jika klien tidak didesain toleran, atau
- sediakan fallback yang terdokumentasi dengan jelas.
Perbedaan perilaku antara sync API dan async result
Masalah lain muncul ketika respons sinkron endpoint berubah tetapi webhook atau polling result masih memakai semantik lama. Klien lama lalu melihat dua sumber kebenaran yang tidak cocok.
Prinsipnya: status model harus konsisten lintas kanal. Jika status=processing di API berarti pekerjaan belum final, webhook juga harus memakai transisi yang kompatibel dengan definisi itu.
Anti-pattern yang harus dihindari
- Versioning untuk setiap perubahan kecil, sehingga biaya operasional naik tanpa menyelesaikan masalah desain.
- Silent breaking change: field tetap sama tetapi tipe atau artinya berubah.
- POST sensitif tanpa idempotency key.
- Retry semua error 5xx dan timeout tanpa batas.
- Error hanya berupa string message tanpa kode stabil.
- Webhook tanpa event ID dan tanpa signature.
- Menganggap delivery exactly-once padahal provider melakukan retry.
- Token rotation tanpa overlap sehingga semua klien harus berpindah serentak.
- Mengandalkan undocumented behavior seperti urutan field, urutan event, atau default implisit.
- Compatibility semu: tidak membuat versi baru, tetapi menumpuk percabangan perilaku per klien sampai sistem tidak bisa dipahami.
Checklist review API sebelum rilis
- Apakah request dan response punya skema eksplisit?
Field wajib, opsional, nullable, enum, format waktu, dan satuan nilai sudah jelas. - Apakah perubahan ini kompatibel?
Bukan hanya pada level JSON, tetapi juga pada makna bisnis, status code, dan perilaku retry. - Apakah operasi sensitif mendukung idempotensi?
Header, masa simpan key, fingerprint payload, dan respons konflik sudah didefinisikan. - Apakah retry policy aman?
Status mana yang boleh di-retry, backoff, jitter, dan batas total percobaan tersedia. - Apakah error model konsisten?
Ada kode error stabil, request ID, dan indikasi apakah error retryable. - Apakah webhook punya event ID, signature, dan strategi retry?
Consumer dapat melakukan deduplikasi dan verifikasi asal pesan. - Apakah auth mendukung rotasi kredensial?
Ada overlap, observability, dan dokumentasi deprecation. - Apakah klien lama sudah diuji?
Gunakan traffic replay, contract test, atau sampel payload produksi yang dianonimkan. - Apakah observability cukup?
Log request ID, idempotency key, event ID, credential ID, dan metrik per versi klien. - Apakah ada rencana sunset?
Tanggal, komunikasi, dan indikator adopsi versi baru jelas.
Kapan breaking change lebih baik daripada kompatibilitas semu
Tidak semua kompatibilitas itu sehat. Ada titik di mana memaksakan perilaku lama justru memperbesar risiko operasional dan membuat rewrite tidak pernah selesai.
Breaking change lebih baik jika:
- Makna domain inti berubah dan tidak bisa direpresentasikan dengan aman dalam skema lama.
- Model auth lama tidak aman atau tidak bisa diputar kredensialnya dengan wajar.
- Error lama terlalu ambigu hingga klien tidak bisa mengambil tindakan yang benar.
- Compatibility shim membuat perilaku berbeda-beda per klien dan sulit diaudit.
Jika harus melakukan breaking change, lakukan dengan disiplin:
- buat versi baru yang tegas,
- dokumentasikan perbedaan perilaku, bukan hanya perbedaan field,
- sediakan masa transisi dan alat observasi migrasi,
- berikan contoh request/response nyata, dan
- ukur siapa yang masih bergantung pada versi lama sebelum benar-benar menutupnya.
Breaking change yang jujur, terdokumentasi, dan terukur sering lebih murah daripada kompatibilitas semu yang membiarkan asumsi lama terus menginfeksi sistem baru.
Penutup
Tren bahwa AI mengubah ekonomi rewrite memang nyata, tetapi ia tidak menghapus biaya kontrak. Semakin banyak pihak di luar proses deploy Anda, semakin penting desain API yang eksplisit dan stabil. Kontrak API anti-rewrite berarti Anda merancang auth, idempotensi, retry, webhook, dan versioning sebagai batas sistem yang tahan terhadap jaringan buruk, klien lama, dan perubahan implementasi internal.
Jika Anda hanya mengambil beberapa langkah, prioritaskan ini: definisikan skema dengan jelas, standarkan error model, wajibkan idempotency key untuk POST sensitif, anggap webhook sebagai at-least-once, siapkan deduplikasi, dan dukung token rotation dengan overlap. Langkah-langkah ini tidak membuat API statis, tetapi membuat perubahan berikutnya jauh lebih murah dan lebih aman.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!