Efek Typed untuk Kontrak API Auth yang Tahan Retry berarti satu hal praktis: perilaku seperti autentikasi, retry, idempotency, timeout, dan delivery webhook tidak boleh disembunyikan di implementasi internal atau diasumsikan oleh client. Semua itu perlu dimodelkan sebagai bagian dari kontrak API yang eksplisit, sehingga client dan server punya aturan yang sama saat menghadapi kegagalan jaringan, token kedaluwarsa, duplicate request, atau side effect eksternal yang lambat.
Ide typed effects dari dunia bahasa pemrograman dapat diterjemahkan ke backend sebagai disiplin desain: setiap endpoint harus menyatakan efek apa yang bisa terjadi, bagaimana efek itu diamati, bagaimana ia gagal, dan apa yang aman untuk di-retry. Bukan teori tipe yang abstrak, melainkan keputusan desain yang bisa dipakai tim backend saat menulis spesifikasi endpoint, middleware auth, queue worker, dan handler webhook.
Mengapa kontrak API sering gagal di kondisi retry
Banyak API tampak benar saat diuji di jalur sukses, lalu rusak saat masuk kondisi nyata:
- POST terduplikasi karena client retry setelah timeout, padahal server sebenarnya sudah memproses request pertama.
- Token kedaluwarsa di tengah retry, sehingga request pertama gagal dengan 401, request kedua memakai token baru, tetapi idempotency atau state internal tidak konsisten.
- Webhook terkirim tidak berurutan, sehingga konsumen menerima status paid sebelum created, atau menerima event lama setelah event baru.
- Operasi tampak sukses padahal side effect gagal, misalnya order tersimpan di database tetapi email, enqueue job, atau panggilan ke payment provider gagal diam-diam.
Akar masalahnya biasanya sama: kontrak API hanya mendeskripsikan input/output utama, tetapi tidak menyatakan efek operasional yang menyertainya. Akibatnya, client menebak-nebak apa yang aman untuk diulang, kapan harus menyegarkan token, apakah respons sukses berarti semua side effect selesai, dan bagaimana menafsirkan webhook yang datang terlambat.
Menerjemahkan typed effects ke desain API backend
Dalam artikel ini, typed effect tidak berarti menambahkan sistem tipe formal ke HTTP. Maksudnya adalah setiap endpoint memiliki “daftar efek” yang dinyatakan secara eksplisit dalam kontrak. Secara praktis, sebuah operasi backend sering memiliki beberapa efek berikut:
- Auth effect: butuh identitas dan otorisasi tertentu; token bisa kedaluwarsa; hasil bisa berbeda antara 401 dan 403.
- Retry effect: client boleh atau tidak boleh mengirim ulang request yang sama.
- Idempotency effect: request yang identik dengan kunci yang sama harus menghasilkan hasil logis yang sama.
- Timeout effect: server atau upstream bisa lambat; hasil akhir bisa tidak diketahui client.
- External side effect: ada panggilan ke payment gateway, email provider, queue, storage, atau layanan lain.
- Webhook delivery effect: sistem mengirim event secara asinkron, mungkin lebih dari sekali, dan belum tentu berurutan.
Dengan cara pandang ini, dokumentasi endpoint bukan hanya “body request dan response schema”, tetapi juga aturan efeknya. Inilah yang membuat kontrak API auth yang tahan retry menjadi jelas dan dapat diuji.
Model kontrak: deklarasikan efek per endpoint
Daripada menulis dokumentasi yang samar seperti “endpoint ini membuat order”, lebih baik nyatakan kontraknya secara eksplisit.
Contoh endpoint: membuat pembayaran
POST /v1/payments
Authorization: Bearer <access-token>
Idempotency-Key: 7f6d0b4e-5a1d-4b7b-8a0f-3f8a8a1a2d11
Content-Type: application/json
{
"order_id": "ord_123",
"amount": 150000,
"currency": "IDR",
"method": "bank_transfer"
}Kontrak endpoint semacam ini sebaiknya menyatakan:
- Auth: memerlukan bearer token valid dengan scope tertentu.
- Retry: aman di-retry hanya jika
Idempotency-Keydisertakan. - Idempotency window: kunci disimpan untuk periode tertentu.
- Side effect: pembuatan pembayaran bisa memicu reservasi di provider eksternal dan enqueue webhook/event internal.
- Completion model: respons
201hanya menjamin resource tercatat, bukan semua notifikasi eksternal selesai.
Anda tidak perlu menyebut ini “typed effects” di dokumentasi publik. Namun secara desain internal, pola pikirnya sama: efek-efek tersebut adalah bagian dari tipe kontrak endpoint.
Skema endpoint yang eksplisit terhadap auth, retry, dan status hasil
Contoh respons sukses dan status transisional
HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/payments/pay_9abc
{
"id": "pay_9abc",
"status": "pending_settlement",
"order_id": "ord_123",
"amount": 150000,
"currency": "IDR",
"request_id": "req_456",
"idempotency_key": "7f6d0b4e-5a1d-4b7b-8a0f-3f8a8a1a2d11"
}Perhatikan bahwa status domain bisa pending_settlement, bukan langsung succeeded. Ini penting agar kontrak tidak berbohong. Jika Anda masih harus memanggil provider eksternal atau menunggu konfirmasi async, jangan mengembalikan status yang menyiratkan semua efek sudah final.
Status code yang sebaiknya dibedakan
- 401 Unauthorized: token tidak valid, hilang, atau kedaluwarsa.
- 403 Forbidden: token valid tetapi tidak memiliki izin yang dibutuhkan.
- 409 Conflict:
Idempotency-Keydigunakan ulang dengan payload berbeda, atau terjadi konflik state. - 422 Unprocessable Entity: payload valid secara sintaks, tetapi tidak lolos aturan domain.
- 202 Accepted: request diterima, tetapi efek utama diproses asinkron.
- 201 Created: resource sudah tercatat, walau side effect sekunder bisa belum selesai.
- 503 Service Unavailable: dependency penting tidak tersedia; biasanya client boleh retry dengan backoff.
Pembedaan ini bukan kosmetik. Client membutuhkan semantik yang konsisten untuk memutuskan apakah ia harus retry, refresh token, memperbaiki payload, atau berhenti total.
Idempotency: jangan mengandalkan niat baik client
Masalah POST terduplikasi hampir selalu muncul saat timeout atau koneksi putus setelah server menerima request tetapi sebelum client menerima respons. Tanpa idempotency, client tidak tahu apakah ia harus mengulang atau tidak.
Header yang umum dipakai
Idempotency-Key: 7f6d0b4e-5a1d-4b7b-8a0f-3f8a8a1a2d11Prinsip implementasinya:
- Server menerima key unik dari client untuk operasi yang secara logis hanya boleh terjadi sekali.
- Server menyimpan fingerprint request yang terkait dengan key tersebut, biasanya meliputi metode, path, tenant/user, dan payload yang dinormalisasi.
- Jika request yang sama datang lagi dengan key yang sama dan fingerprint yang sama, server mengembalikan hasil yang sama atau representasi status yang ekuivalen.
- Jika key yang sama dipakai dengan payload berbeda, server mengembalikan
409 Conflict.
Detail pentingnya adalah cakupan key. Idempotency key sebaiknya tidak global tanpa konteks. Ikat ia minimal ke identitas pemilik request dan endpoint, agar tabrakan lintas tenant atau lintas operasi tidak terjadi.
Contoh perilaku saat duplikasi
HTTP/1.1 201 Created
Content-Type: application/json
Idempotency-Replayed: true
{
"id": "pay_9abc",
"status": "pending_settlement",
"order_id": "ord_123"
}Header seperti Idempotency-Replayed: true membantu debugging, walau nama header spesifiknya bisa Anda tentukan sendiri. Yang penting adalah semantiknya jelas.
Catatan: idempotency bukan pengganti transaksi domain yang benar. Jika operasi internal Anda menyentuh beberapa tabel dan external provider, Anda tetap perlu boundary transaksi yang jelas dan strategi penyimpanan state yang tahan retry.
Auth yang tahan retry: 401 bukan sekadar gagal login
Pada sistem nyata, retry dan auth saling mempengaruhi. Misalnya, request pertama timeout. Client mencoba lagi, tetapi access token sudah kedaluwarsa. Jika kontrak API tidak jelas, client bisa mengirim request baru dengan token baru tanpa memahami apakah request sebelumnya sudah sempat diproses.
Aturan praktis yang perlu dinyatakan
- Autentikasi dievaluasi per request, bukan per idempotency key. Request replay dengan token baru tetap boleh memetakan ke operasi yang sama, selama identitas dan hak aksesnya masih cocok.
- Idempotency harus diikat ke principal yang stabil, misalnya account atau client application, bukan ke token mentah. Token bisa berubah karena refresh.
- 401 harus berarti client dapat memperbaiki dengan kredensial baru. Jika sebenarnya masalahnya izin, gunakan 403.
- Jika token kedaluwarsa setelah operasi sempat dijalankan, server tetap harus mampu mengembalikan hasil operasi yang sama ketika request diulang dengan token baru dan idempotency key yang sama.
Dengan kata lain, token adalah mekanisme pembuktian identitas, bukan identitas operasional itu sendiri. Ini penting agar retry setelah token refresh tidak menciptakan duplikasi resource.
Contoh alur yang benar
- Client mengirim
POST /v1/paymentsdengan token A dan idempotency key K. - Server memproses dan menyimpan hasil, tetapi koneksi putus sebelum respons diterima client.
- Client retry, token A sudah kedaluwarsa dan memperoleh token B.
- Client mengirim request yang sama dengan token B dan key K.
- Server memvalidasi token B, memetakan key K ke hasil yang sudah ada, lalu mengembalikan respons yang sama tanpa membuat pembayaran kedua.
Jika sistem Anda mengikat idempotency key ke token, alur di atas akan gagal dan duplikasi sangat mungkin terjadi.
Timeout dan hasil yang tidak diketahui
Salah satu kesalahan kontrak API yang paling mahal adalah menganggap timeout sama dengan gagal total. Dari sudut pandang client, timeout sering berarti unknown outcome: request mungkin gagal, mungkin juga sukses tetapi responsnya tidak sempat diterima.
Desain kontrak untuk kondisi unknown
- Gunakan idempotency key untuk operasi tulis yang bisa mahal jika terduplikasi.
- Sediakan endpoint pengecekan status berdasarkan resource ID atau idempotency key.
- Bedakan antara penerimaan request dan penyelesaian side effect.
- Hindari menyatukan semua kegagalan ke satu pesan generik seperti “something went wrong”.
Contoh endpoint status
GET /v1/payments/by-idempotency-key/7f6d0b4e-5a1d-4b7b-8a0f-3f8a8a1a2d11
Authorization: Bearer <access-token>Endpoint seperti ini berguna ketika client tidak yakin apakah operasi sudah tercatat. Dibanding mengulang POST membabi buta, client bisa menanyakan status hasil yang sudah diketahui server.
Side effect eksternal: jangan laporkan sukses lebih awal
Masalah klasik lain adalah respons 200 atau 201 yang tampak sukses, padahal hanya transaksi database lokal yang berhasil; enqueue job gagal, publish event gagal, atau panggilan ke provider eksternal belum pernah terjadi. Ini membuat sistem tampak sehat di sisi API, tetapi data di ekosistem menjadi tidak sinkron.
Prinsip desain yang lebih aman
- Nyatakan apa arti sukses. Apakah resource sudah disimpan saja, atau seluruh side effect penting sudah berhasil?
- Pisahkan efek sinkron dan asinkron. Jika notifikasi atau webhook bersifat eventual, jangan sematkan makna final ke respons awal.
- Simpan state transisional seperti
pending,processing,dispatched,failed_delivery. - Gunakan pola outbox atau mekanisme serupa agar commit database dan publikasi event tidak saling kehilangan.
Untuk banyak sistem backend, pola yang masuk akal adalah:
- Simpan perubahan domain dan catatan event/outbox dalam satu transaksi lokal.
- Kembalikan respons yang jujur, misalnya resource dibuat dengan status
pending. - Worker terpisah mengirim side effect eksternal berdasarkan outbox.
- Update status delivery dan sediakan observability yang cukup.
Pola ini membuat efek eksternal menjadi eksplisit dalam kontrak, bukan asumsi tersembunyi di balik respons sukses.
Webhook delivery sebagai efek eksplisit, bukan bonus fitur
Webhook sering didokumentasikan terlalu tipis, padahal ia adalah kontrak distribusi event. Dalam praktiknya, webhook bisa:
- terkirim lebih dari sekali,
- terkirim tidak berurutan,
- tertunda lama,
- gagal sementara lalu di-retry.
Karena itu, kontraknya harus menyatakan semantik delivery, bukan hanya payload JSON.
Skema event webhook yang lebih aman
{
"event_id": "evt_01",
"event_type": "payment.updated",
"created_at": "2026-06-28T10:00:00Z",
"delivery_attempt": 3,
"resource_id": "pay_9abc",
"resource_version": 7,
"data": {
"id": "pay_9abc",
"status": "settled",
"amount": 150000,
"currency": "IDR"
}
}Beberapa bidang kunci:
- event_id: untuk deduplikasi di sisi konsumen.
- resource_id: mengaitkan event ke entitas domain.
- resource_version: membantu konsumen mendeteksi event yang datang tidak berurutan.
- delivery_attempt: memudahkan observability dan troubleshooting.
Aturan kontrak webhook yang perlu ditulis jelas
- Webhook at least once, sehingga konsumen harus idempotent.
- Urutan delivery tidak dijamin kecuali Anda benar-benar mampu menjaminnya secara teknis.
- Konsumen sebaiknya memproses berdasarkan versi resource atau timestamp domain, bukan urutan kedatangan HTTP.
- Respons non-2xx dari konsumen dianggap gagal dan dapat memicu retry.
Jika urutan penting untuk use case tertentu, lebih aman menyediakan endpoint pull untuk mengambil state final resource daripada berharap webhook selalu datang berurutan.
Tabel failure mode yang sebaiknya ada di kontrak API
| Skenario | Akar Masalah | Risiko | Kontrak yang Disarankan |
|---|---|---|---|
| POST timeout lalu client retry | Hasil request pertama tidak diketahui client | Resource terbuat dua kali | Wajibkan Idempotency-Key untuk operasi tulis kritis |
| Retry dengan token baru | Access token kedaluwarsa di antara percobaan | Duplikasi atau 401 yang membingungkan | Ikat idempotency ke principal stabil, bukan ke token |
| Key sama, payload berbeda | Bug client atau reuse key yang salah | State ambigu | Kembalikan 409 Conflict dan simpan fingerprint request |
| Webhook datang terlambat | Retry delivery atau antrian lambat | Konsumen meng-overwrite state baru dengan state lama | Sertakan resource_version atau penanda monotonic lain |
| Webhook duplikat | Delivery at least once | Efek samping terpicu dua kali di konsumen | Sertakan event_id; konsumen wajib deduplikasi |
| DB commit sukses, publish event gagal | Partial failure antar sistem | API tampak sukses, integrasi gagal diam-diam | Gunakan outbox atau state transisional yang eksplisit |
| Dependency eksternal lambat | Timeout upstream | Latensi tinggi, hasil tidak pasti | Gunakan 202 atau status pending bila penyelesaian final asinkron |
| 401 dan 403 disamakan | Semantik auth tidak jelas | Client retry atau refresh token secara salah | Bedakan kegagalan autentikasi dan otorisasi secara konsisten |
Contoh kontrak endpoint yang bisa direview tim backend
Format di bawah ini berguna sebagai template internal saat mendesain endpoint baru.
Endpoint: POST /v1/payments
Auth effect:
- Bearer token wajib
- Scope: payments:write
- 401 jika token invalid/kedaluwarsa
- 403 jika principal tidak diizinkan membuat pembayaran
Retry effect:
- Aman di-retry hanya jika header Idempotency-Key ada
- Retry tanpa key tidak dijamin aman
Idempotency effect:
- Key unik per principal + endpoint
- Payload harus identik untuk key yang sama
- Jika key sama, payload berbeda: 409 Conflict
Timeout effect:
- Jika client timeout, hasil operasi mungkin unknown
- Client dapat memeriksa status via GET /v1/payments/by-idempotency-key/{key}
External side effect:
- Pembuatan payment dapat memicu reservasi provider dan publish event
- Respons 201 berarti resource tercatat; side effect lanjutan bisa masih diproses
Webhook effect:
- Event payment.updated dikirim at least once
- Urutan delivery tidak dijamin
- Konsumen harus deduplikasi berdasarkan event_idTemplate seperti ini memaksa tim untuk mengubah asumsi tersembunyi menjadi keputusan eksplisit.
Kesalahan umum saat menerapkan kontrak efek eksplisit
1. Menganggap idempotency cukup dengan unique index
Unique constraint memang berguna, tetapi tidak cukup untuk memberikan pengalaman retry yang baik. Ia biasanya hanya mencegah duplikasi akhir, bukan mengembalikan hasil yang konsisten atau menjelaskan apakah request sebelumnya sukses.
2. Menyimpan idempotency key tanpa fingerprint request
Jika server hanya menyimpan key lalu menganggap semua request dengan key yang sama identik, client yang bug bisa tanpa sengaja menggunakan key lama untuk payload baru. Anda perlu membandingkan isi request yang relevan.
3. Mengaitkan semantik bisnis ke sukses transport
HTTP 200 hanya berarti server mengembalikan respons sukses menurut kontrak. Ia tidak otomatis berarti seluruh proses bisnis final. Jika state masih eventual, respons harus mencerminkan itu.
4. Menjanjikan ordering webhook tanpa mekanisme kuat
Begitu ada retry, multi-worker, atau beberapa partition, ordering menjadi sulit dijaga. Jangan menjanjikan “selalu berurutan” jika sistem Anda tidak benar-benar dirancang untuk itu.
5. Menyamakan semua retry
Retry oleh client, API gateway, job worker, dan webhook sender bisa memiliki aturan berbeda. Kontrak harus menyatakan siapa yang boleh retry, kapan, dan dengan syarat apa.
Checklist review kontrak API
Sebelum endpoint auth-sensitive dan write-heavy dipublikasikan, gunakan checklist ini:
- Apakah endpoint secara eksplisit menyatakan apakah aman untuk di-retry?
- Apakah operasi tulis kritis mendukung
Idempotency-Key? - Apakah idempotency diikat ke principal stabil, bukan token yang mudah berubah?
- Apakah
401dan403dibedakan dengan jelas? - Apakah ada definisi yang jujur tentang arti respons sukses?
- Apakah timeout menghasilkan status “unknown” yang bisa dipulihkan client?
- Apakah tersedia endpoint atau mekanisme untuk memeriksa status pasca-timeout?
- Apakah side effect eksternal dimodelkan eksplisit, termasuk kemungkinan tertunda atau gagal?
- Apakah webhook didokumentasikan sebagai at-least-once atau semantik delivery lain yang realistis?
- Apakah payload webhook punya
event_iddan penanda versi/resource ordering? - Apakah reuse idempotency key dengan payload berbeda menghasilkan error yang jelas?
- Apakah observability tersedia: request ID, delivery attempt, status transisional, dan log korelasi?
Debugging dan observability yang membantu
Kontrak yang baik tetap perlu alat debugging yang sesuai. Minimal, pertimbangkan hal berikut:
- Request ID di setiap respons untuk korelasi log.
- Idempotency replay marker untuk mengetahui apakah hasil berasal dari replay.
- Status transisional yang dapat di-query, bukan hanya boolean sukses/gagal.
- Audit trail ringkas untuk perubahan state penting dan delivery webhook.
- Metric per failure mode, misalnya jumlah 401 setelah retry, konflik idempotency, webhook duplicate, dan outbox lag.
Debugging menjadi jauh lebih mudah jika kontrak sudah memisahkan kegagalan auth, konflik idempotency, dependency timeout, dan kegagalan side effect. Tanpa itu, semua masalah terlihat sama dari sisi client padahal akar teknisnya berbeda.
Penutup
Mengadaptasi ide typed effects ke backend bukan berarti membawa teori bahasa pemrograman mentah-mentah ke HTTP. Intinya lebih sederhana dan lebih berguna: jadikan auth, retry, idempotency, timeout, webhook delivery, dan side effect eksternal sebagai kontrak eksplisit. Dengan begitu, API tidak hanya benar di jalur sukses, tetapi juga tetap dapat diprediksi saat jaringan bermasalah, token berganti, webhook datang ganda, atau dependency eksternal gagal sebagian.
Jika tim backend hanya mengambil satu praktik dari artikel ini, pilih yang ini: untuk setiap endpoint tulis, dokumentasikan dengan jelas apa yang aman di-retry, identitas apa yang dipakai untuk idempotency, apa arti sukses, dan efek apa yang masih bisa terjadi setelah respons dikirim. Itulah bentuk paling praktis dari Efek Typed untuk Kontrak API Auth yang Tahan Retry.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!