Kontrak error API yang stabil menentukan apakah integrasi antarsistem akan mudah dipelihara atau justru rapuh setiap kali ada perubahan kecil. Jika error hanya berupa pesan teks yang berubah-ubah, client sulit membedakan mana kegagalan validasi, mana kondisi sementara yang boleh di-retry, dan mana error internal yang harus diinvestigasi.
Solusi yang aman adalah mendefinisikan response error yang konsisten, machine-readable, dan terdokumentasi. Artinya, status HTTP digunakan dengan tepat, body error memiliki kode yang stabil, ada request/correlation ID untuk pelacakan, detail validasi tersusun rapi, serta ada penanda apakah request layak di-retry. Dengan pola ini, integrasi untuk aplikasi web, mobile, webhook consumer, hingga background worker menjadi lebih dapat diprediksi.
Mengapa kontrak error API perlu stabil
Dalam integrasi nyata, pihak pemanggil API jarang hanya menampilkan pesan error ke pengguna. Mereka biasanya harus mengambil keputusan teknis, misalnya:
- apakah request perlu diulang otomatis,
- apakah error harus masuk ke dead-letter queue,
- apakah webhook perlu dianggap gagal dan dicoba ulang,
- apakah insiden perlu dinaikkan ke sistem monitoring,
- atau apakah pengguna perlu diminta memperbaiki input.
Keputusan-keputusan itu tidak boleh bergantung pada string seperti "Email tidak valid" atau "Terjadi kesalahan", karena teks mudah berubah akibat refactor, lokalisasi, atau penyesuaian copywriting. Yang harus stabil adalah struktur dan kode error-nya.
Kontrak error yang baik memberi dua lapisan informasi:
- Untuk mesin: status HTTP, error code, retryable flag, field validation detail, dan ID pelacakan.
- Untuk manusia: pesan singkat yang membantu debugging tanpa membocorkan detail sensitif.
Struktur response error yang direkomendasikan
Tidak ada satu standar tunggal yang wajib dipakai di semua sistem, tetapi bentuk berikut praktis dan mudah dipahami lintas tim. Yang paling penting adalah stabilitas field, bukan nama persisnya.
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Permintaan tidak valid.",
"retryable": false,
"request_id": "req_7f3c9b2a",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Format email tidak valid."
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Nilai age harus antara 18 dan 65."
}
]
}
}Field yang umumnya berguna:
- code: kode error stabil untuk logika client, misalnya
VALIDATION_FAILED,RATE_LIMITED,RESOURCE_NOT_FOUND,INTERNAL_ERROR. - message: penjelasan singkat untuk manusia. Jangan jadikan ini dasar branching di client.
- retryable: boolean yang membantu worker atau integrator menentukan apakah request layak dicoba ulang.
- request_id: ID unik untuk korelasi log, tracing, dan tiket support.
- details: rincian tambahan, terutama untuk validasi field atau error domain yang memiliki beberapa sub-item.
Prinsip desain field
- Jangan ganti makna field tanpa versi baru. Jika
retryableberarti “aman untuk retry otomatis”, jangan ubah menjadi “mungkin bisa dicoba lagi secara manual”. - Jangan hapus field penting diam-diam. Client lama bisa rusak meski response masih JSON valid.
- Tambahkan field baru secara kompatibel. Penambahan biasanya aman jika client diharapkan mengabaikan field yang tidak dikenal.
- Hindari detail internal sensitif, seperti stack trace, query database, path file, atau nama service internal.
Status HTTP yang tepat dan dokumentasi perilaku 4xx vs 5xx
Status HTTP harus merepresentasikan kategori kegagalan. Ini penting karena banyak client, proxy, gateway, job runner, dan SDK mengambil keputusan dasar dari status code sebelum membaca body.
Klasifikasi praktis error
| Kondisi | Status HTTP | Contoh code | Retry? | Catatan |
|---|---|---|---|---|
| Payload tidak valid | 400 atau 422 | VALIDATION_FAILED | Tidak | Gunakan detail per field agar client bisa memperbaiki input. |
| Autentikasi gagal | 401 | UNAUTHORIZED | Tidak langsung | Bisa berhasil setelah token diperbarui. |
| Akses dilarang | 403 | FORBIDDEN | Tidak | Bukan masalah sementara. |
| Resource tidak ditemukan | 404 | RESOURCE_NOT_FOUND | Tergantung konteks | Biasanya tidak, kecuali ada eventual consistency. |
| Konflik state/idempoten | 409 | CONFLICT | Tergantung konteks | Cocok untuk duplikasi atau perubahan state yang bentrok. |
| Rate limit | 429 | RATE_LIMITED | Ya | Idealnya sertakan petunjuk jeda retry. |
| Gangguan downstream/internal | 500 | INTERNAL_ERROR | Ya, hati-hati | Retry otomatis sebaiknya dibatasi dan memakai backoff. |
| Service sementara tidak siap | 503 | SERVICE_UNAVAILABLE | Ya | Lebih jelas daripada semua kegagalan dijadikan 500. |
| Timeout upstream/gateway | 504 | UPSTREAM_TIMEOUT | Ya | Menandakan kegagalan sementara, bukan input salah. |
Kapan memakai 400 dan kapan 422
Dalam praktik, keduanya sering dipakai untuk input salah. Yang penting adalah konsistensi di seluruh API. Jika tim memilih:
- 400 untuk request yang salah secara umum, gunakan secara konsisten.
- 422 untuk payload yang secara struktur bisa diparse tetapi gagal validasi bisnis/field, dokumentasikan aturan itu dengan jelas.
Masalah terbesar bukan memilih 400 atau 422, melainkan mencampur keduanya tanpa aturan yang jelas.
Dokumentasikan perilaku 4xx vs 5xx
Aturan yang mudah dipahami oleh integrator:
- 4xx berarti ada yang perlu diperbaiki di sisi pemanggil: input, autentikasi, otorisasi, atau state request.
- 5xx berarti kegagalan sementara atau internal di sisi penyedia API atau dependensinya.
Untuk integrasi yang aman, dokumentasikan bukan hanya status code, tetapi juga aksi yang diharapkan: apakah client harus memperbaiki request, memperbarui kredensial, menunggu dan retry, atau menghubungi support dengan request ID.
Machine-readable error code: fondasi integrasi yang tahan perubahan
Error code adalah kontrak utama untuk percabangan logika. Client bisa menulis aturan yang stabil seperti:
- jika
code=RATE_LIMITED, tunggu lalu retry, - jika
code=VALIDATION_FAILED, tampilkan error per field, - jika
code=UNAUTHORIZED, lakukan refresh token atau minta login ulang, - jika
code=INTERNAL_ERROR, catat request ID dan jalankan retry terbatas.
Karakteristik error code yang baik:
- Stabil: tidak berubah hanya karena teks pesan diubah.
- Cukup umum: jangan terlalu granular jika belum benar-benar dibutuhkan.
- Berorientasi aksi: memudahkan client mengambil keputusan.
Contoh hierarki yang praktis:
{
"error": {
"code": "RATE_LIMITED",
"message": "Terlalu banyak request.",
"retryable": true,
"request_id": "req_ab12cd34"
}
}Untuk validasi field, gunakan kode global dan kode detail per item:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Permintaan tidak valid.",
"retryable": false,
"request_id": "req_91ef23aa",
"details": [
{ "field": "phone", "code": "REQUIRED", "message": "phone wajib diisi." },
{ "field": "email", "code": "INVALID_FORMAT", "message": "Format email tidak valid." }
]
}
}Pola ini memudahkan UI menandai field bermasalah, sekaligus memudahkan sistem otomatis mengenali bahwa kategori error tetap VALIDATION_FAILED.
Correlation ID dan request ID untuk debugging dan observability
Ketika integrasi melibatkan API gateway, beberapa service internal, queue, dan webhook, satu pesan error tanpa ID pelacakan hampir tidak berguna untuk investigasi. Karena itu, sertakan request ID di response error dan, bila memungkinkan, terima atau propagasikan correlation ID dari request masuk.
Perbedaan praktisnya
- Request ID: unik untuk satu request yang diproses API.
- Correlation ID: menghubungkan beberapa request atau event dalam satu alur bisnis yang sama.
Dalam banyak sistem, satu ID saja sudah cukup untuk tahap awal, selama ia konsisten muncul di response, log aplikasi, tracing, dan dashboard observability.
Contoh header dan body:
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
X-Request-Id: req_f6a8102c
{
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "Layanan sedang tidak tersedia.",
"retryable": true,
"request_id": "req_f6a8102c"
}
}Mengapa tetap ada di header dan body?
- Header memudahkan middleware, reverse proxy, dan tooling HTTP.
- Body memudahkan client yang menyimpan payload error apa adanya ke log atau audit trail.
Dampak ke observability
- Logging: tim support bisa mencari semua log terkait
request_id. - Tracing: lebih mudah melihat titik gagal di service internal.
- Alerting: error 5xx dapat dikelompokkan berdasarkan
codedan endpoint, bukan hanya status mentah. - Webhook troubleshooting: consumer bisa melaporkan ID spesifik saat meminta investigasi.
Retryability flag: kapan client boleh mencoba ulang
Tidak semua client membaca status HTTP dengan cermat, dan tidak semua status code cukup untuk keputusan retry yang aman. Menambahkan field retryable membantu menyederhanakan integrasi, terutama untuk worker, scheduler, dan webhook consumer.
Contoh error yang layak di-retry:
SERVICE_UNAVAILABLEUPSTREAM_TIMEOUTRATE_LIMITED
Contoh yang umumnya tidak layak di-retry tanpa perubahan request:
VALIDATION_FAILEDFORBIDDENRESOURCE_NOT_FOUNDdalam sistem tanpa eventual consistency
Catatan penting tentang retry
- Retryable tidak selalu berarti aman diulang tanpa desain idempoten.
- Untuk operasi yang membuat data, pertimbangkan idempotency key agar retry tidak menyebabkan duplikasi.
- Gunakan exponential backoff dan batas maksimum retry agar tidak memperparah kegagalan sistem.
Contoh logika sederhana di worker:
if response.status in [500, 503, 504] and error.retryable == true:
retry_with_backoff()
elif response.status == 429 and error.retryable == true:
retry_after_delay()
else:
move_to_dead_letter_or_fail()Walau pseudo-code ini sederhana, prinsipnya jelas: keputusan retry harus berdasar sinyal yang stabil, bukan regex atas teks pesan.
Dampak pada client, webhook consumer, dan worker
Client aplikasi
Client frontend atau mobile membutuhkan pembedaan yang jelas antara error yang bisa diperbaiki pengguna dan error sistem. Dengan kontrak yang stabil:
- field validasi bisa ditampilkan tepat di form,
- autentikasi kadaluwarsa bisa diarahkan ke refresh token atau login ulang,
- error 5xx bisa ditampilkan sebagai gangguan sementara tanpa menyalahkan input pengguna.
Webhook consumer
Webhook biasanya berjalan tanpa interaksi manusia. Consumer harus memutuskan apakah event akan diproses ulang atau diparkir untuk investigasi. Jika semua respons sukses/gagal disamarkan, misalnya selalu 200 dengan body {"success":false}, maka platform pengirim bisa salah menganggap delivery berhasil dan berhenti retry.
Untuk webhook, status HTTP sangat penting:
- kembalikan 2xx hanya jika event benar-benar diterima dan diproses atau minimal diterima untuk diproses aman,
- gunakan 4xx bila payload tidak valid atau otorisasi salah,
- gunakan 5xx untuk gangguan sementara agar pengirim bisa retry sesuai kebijakannya.
Background worker dan queue consumer
Worker membutuhkan sinyal yang dapat diotomatisasi. Kontrak error yang baik membantu menentukan:
- langsung gagal permanen,
- retry dengan backoff,
- kirim ke dead-letter queue,
- atau eskalasi insiden.
Tanpa kode error yang stabil, worker sering berakhir memakai aturan rapuh seperti mencocokkan substring "timeout" atau "temporarily unavailable" dari pesan error.
Anti-pattern yang sering merusak integrasi
1. Semua error dikembalikan sebagai 200
Ini memaksa client membaca body untuk mengetahui kegagalan dan sering merusak integrasi dengan gateway, cache, observability, dan webhook retrier yang mengandalkan status HTTP.
2. Semua kegagalan dijadikan 500
Akibatnya, input yang salah tampak seperti masalah server. Client bisa terus retry request yang sebenarnya tidak akan pernah berhasil.
3. Mengandalkan message untuk percabangan logika
Pesan berubah karena lokalisasi, koreksi ejaan, atau perubahan gaya bahasa. Begitu teks berubah, client yang melakukan pencocokan string ikut rusak.
4. Struktur error berbeda-beda antar endpoint
Misalnya endpoint A mengirim {error: "..."}, endpoint B mengirim {message: "..."}, endpoint C mengirim array. Integrator harus menulis parser khusus per endpoint, padahal kategori error serupa.
5. Tidak ada request ID
Tanpa ID pelacakan, investigasi menjadi lambat karena tim harus menebak log mana yang sesuai dengan laporan client.
6. Validasi hanya satu pesan umum tanpa detail field
UI dan API consumer kesulitan memberi umpan balik yang spesifik. Hasilnya, pengguna harus menebak field mana yang salah.
7. Membocorkan detail internal
Stack trace, query SQL, nama tabel, atau detail service internal bisa menambah risiko keamanan dan tidak membantu integrator umum.
Contoh kontrak error yang lebih lengkap
Contoh berikut cukup kaya untuk banyak kebutuhan integrasi, tetapi masih sederhana untuk dipertahankan dalam jangka panjang.
{
"error": {
"code": "RATE_LIMITED",
"message": "Terlalu banyak request.",
"retryable": true,
"request_id": "req_c1d2e3f4",
"details": {
"scope": "api_key",
"limit_type": "per_minute"
}
}
}Untuk validasi:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Permintaan tidak valid.",
"retryable": false,
"request_id": "req_55aa77bb",
"details": [
{
"field": "customer_id",
"code": "REQUIRED",
"message": "customer_id wajib diisi."
},
{
"field": "amount",
"code": "MIN_VALUE",
"message": "amount harus lebih besar dari 0."
}
]
}
}Perhatikan bahwa details bisa berupa array untuk validasi banyak field, atau objek untuk metadata tambahan pada error non-validasi. Jika memilih pola ini, dokumentasikan dengan tegas agar client tahu kapan ia menerima array dan kapan objek. Alternatif yang lebih aman adalah memisahkan menjadi field berbeda seperti validation_errors dan metadata.
Panduan implementasi agar kompatibel saat API berkembang
1. Definisikan katalog error pusat
Simpan daftar error code, arti, status HTTP, dan retryability di satu tempat. Ini mencegah setiap endpoint membuat istilah sendiri.
2. Pisahkan pesan manusia dari kontrak mesin
Biarkan message berubah seperlunya, tetapi pastikan code, struktur field, dan makna semantiknya stabil.
3. Gunakan middleware atau komponen bersama
Pembuatan response error sebaiknya tidak diulang manual di setiap controller atau handler. Komponen bersama memudahkan konsistensi header, request ID, dan format body.
4. Dokumentasikan contoh sukses dan gagal
Integrator biasanya lebih terbantu oleh contoh payload nyata daripada definisi abstrak. Sertakan contoh 400, 401, 404, 429, dan 5xx yang paling relevan.
5. Perlakukan perubahan kontrak error sebagai perubahan API
Mengganti nama code, menghapus field, atau mengubah tipe data harus diperlakukan seperti perubahan kontrak lain. Jangan anggap aman hanya karena endpoint utama tidak berubah.
6. Rancang untuk forward compatibility
Client sebaiknya diajarkan untuk mengabaikan field baru yang tidak dikenal. Ini memberi ruang evolusi tanpa langsung mematahkan integrasi lama.
Checklist implementasi
- Setiap error memakai status HTTP yang sesuai.
- Body error memiliki struktur konsisten di semua endpoint.
- Ada
error.codeyang stabil dan machine-readable. - Ada
error.messageyang aman untuk manusia. - Ada
error.request_iddan, jika relevan, header request ID. - Ada
error.retryableuntuk membantu automasi retry. - Error validasi menyertakan detail per field.
- Dokumentasi menjelaskan arti 4xx vs 5xx dan aksi yang diharapkan client.
- Response tidak membocorkan detail internal sensitif.
- Ada katalog error code yang dikelola lintas tim.
- Worker dan webhook consumer diuji terhadap skenario retry dan non-retry.
- Perubahan kontrak error masuk ke proses review API, bukan hanya review implementasi.
Checklist pengujian kontrak error
Contract testing
- Verifikasi field wajib selalu ada:
code,message,request_id, dan field lain yang ditetapkan tim. - Verifikasi tipe data tidak berubah, misalnya
retryabletetap boolean. - Verifikasi endpoint yang serupa mengembalikan format error yang seragam.
Integration testing
- Uji bahwa 4xx tidak memicu retry otomatis di worker.
- Uji bahwa 429/503/504 memicu retry dengan backoff.
- Uji bahwa request ID tercatat di log dan dapat ditelusuri.
Negative testing
- Kirim payload kosong, field salah tipe, token invalid, dan konflik state.
- Simulasikan kegagalan downstream untuk memastikan 5xx dan
retryablesesuai.
Backward compatibility testing
- Pastikan penambahan field baru tidak mematahkan client lama.
- Pastikan perubahan pesan teks tidak memengaruhi logic client karena branch berbasis
code.
Penutup
Kontrak Error API yang Stabil untuk Retry dan Integrasi Aman bukan sekadar soal format JSON yang rapi. Ini adalah fondasi agar client, webhook consumer, worker, dan sistem observability bisa mengambil keputusan yang benar tanpa bergantung pada asumsi rapuh. Gunakan status HTTP secara semantik, sediakan error code yang stabil, sertakan request ID, tampilkan detail validasi yang terstruktur, dan dokumentasikan kapan 4xx harus diperbaiki versus kapan 5xx boleh di-retry.
Jika Anda hanya memperbaiki satu hal mulai hari ini, perbaiki dua komponen inti ini: status HTTP yang benar dan machine-readable error code yang stabil. Dari sana, retry policy, debugging, dan evolusi API akan menjadi jauh lebih aman.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!