Hindari abstraksi dini pada API auth dan webhook ketika kebutuhan endpoint belum benar-benar sama. Dalam integrasi API, masalah umum bukan kurangnya generalisasi, tetapi generalisasi yang dipaksakan terlalu cepat: satu kontrak generik dipakai untuk login, token refresh, callback webhook, retry, dan idempotency, padahal sifat operasionalnya berbeda.
Akibatnya biasanya terlihat jelas: field serba opsional, header ambigu, status code tidak konsisten, retry logic bocor ke klien, dan webhook makin sulit diverifikasi. Prinsip yang sejalan dengan gagasan Sandi Metz tentang wrong abstraction adalah sederhana: duplikasi kecil yang jelas sering lebih aman daripada abstraksi besar yang salah. Untuk API integrasi, itu berarti lebih baik punya kontrak yang spesifik per alur daripada satu bentuk request/response universal yang membingungkan.
Mengapa abstraksi dini sering gagal pada auth dan webhook
Auth API dan webhook terlihat mirip hanya di permukaan: keduanya memakai HTTP, header, body JSON, dan kadang tanda tangan digital. Namun karakter dasarnya berbeda.
- Auth bersifat sinkron, dimulai oleh klien, dan hasilnya langsung memengaruhi akses.
- Webhook bersifat asinkron, dimulai oleh server pengirim, dan harus tahan terhadap keterlambatan, duplikasi, dan verifikasi integritas pesan.
- Retry pada auth biasanya tidak sama dengan retry pada webhook. Login gagal karena password salah tidak boleh diulang otomatis, sedangkan delivery webhook gagal karena timeout justru perlu diulang.
- Idempotency untuk endpoint mutasi klien biasanya berbasis kunci permintaan, sedangkan webhook inbound lebih sering bergantung pada event ID atau delivery ID.
Ketika semua dipaksa masuk ke satu kontrak generik, tim mulai menambah pengecualian: field ini wajib hanya untuk tipe tertentu, header itu kadang dipakai untuk signature kadang untuk token, response 200 dipakai untuk semua kasus walau semantiknya berbeda. Dari sini desain mulai bocor.
Gejala abstraction yang salah
1. Field serba opsional
Ini biasanya gejala paling cepat terlihat. Request body menjadi satu objek besar dengan banyak field opsional karena harus bisa melayani beberapa alur sekaligus.
{
"type": "auth_or_webhook",
"token": "...",
"username": "...",
"password": "...",
"signature": "...",
"event_id": "...",
"idempotency_key": "...",
"retry_count": 1
}Masalahnya bukan hanya estetika. Validasi menjadi bercabang dan sulit dipahami:
- Jika
type=auth,usernamedanpasswordwajib. - Jika
type=webhook,signaturedanevent_idwajib. - Jika
type=refresh,tokenwajib, tetapipasswordtidak boleh ada.
Semakin banyak mode, semakin besar peluang bug validasi, dokumentasi salah, dan implementasi klien tidak konsisten.
2. Header ambigu
Contoh umum: satu header seperti X-Auth dipakai untuk bearer token di beberapa endpoint, tetapi di webhook dipakai untuk signature atau shared secret. Ini membuat middleware sulit ditebak dan membuka ruang salah konfigurasi.
Header sebaiknya memiliki makna tunggal. Misalnya:
Authorization: Bearer ...untuk auth berbasis token.X-Signatureatau nama header yang jelas untuk verifikasi webhook.Idempotency-Keyhanya pada endpoint yang memang mendukung idempotent write dari klien.
Dengan pemisahan semantik yang tegas, logika keamanan dan observability juga lebih mudah.
3. Status code tidak konsisten
Wrong abstraction sering mendorong tim menyeragamkan response terlalu jauh, misalnya semua endpoint selalu mengembalikan 200 OK dengan body {"success": true|false}. Padahal status code HTTP membawa informasi operasional penting.
- 401/403 berguna untuk auth failure.
- 400 cocok untuk request tidak valid.
- 409 bisa relevan untuk konflik idempotency tertentu.
- 202 tepat untuk proses asinkron yang diterima tetapi belum selesai.
- 2xx pada webhook sering berarti event sudah diterima dan tidak perlu di-retry oleh pengirim.
- 5xx menandakan kegagalan sementara dan umumnya memicu retry dari pengirim webhook.
Kalau webhook invalid signature tetap dibalas 200 demi “konsistensi kontrak”, Anda kehilangan sinyal penting untuk pengirim dan untuk debugging internal.
4. Logika retry bocor ke tempat yang salah
Retry tidak seharusnya menjadi properti generik yang dipaksakan ke semua endpoint. Auth dan webhook punya alasan retry yang berbeda.
- Login gagal karena kredensial salah: jangan retry otomatis.
- Refresh token gagal karena token kadaluarsa: retry biasanya tidak berguna tanpa interaksi baru.
- Webhook processing gagal karena database timeout: retry masuk akal, tetapi harus dikendalikan oleh pengirim, queue internal, atau keduanya secara eksplisit.
Gejalanya muncul saat API mulai meminta klien mengirim retry_count, is_retry, atau flag generik lain hanya agar server bisa menebak perilaku. Ini menandakan kontrak terlalu umum dan tanggung jawab antar sistem tidak jelas.
5. Webhook sulit diverifikasi
Webhook inbound biasanya perlu verifikasi atas payload mentah, timestamp, dan signature. Jika kontrak generik memaksa webhook diperlakukan seperti request JSON biasa yang langsung diparse dan dimasukkan ke model umum, proses verifikasi menjadi rapuh.
Kesalahan yang sering terjadi:
- Memverifikasi signature dari JSON yang sudah di-serialize ulang, bukan dari raw request body.
- Menggabungkan auth middleware pengguna biasa dengan middleware webhook.
- Menganggap webhook identik dengan API client request, padahal trust model-nya berbeda.
Webhook perlu jalur yang spesifik: ambil body mentah, validasi timestamp jika dipakai, cek signature, baru parse payload dan proses event.
Contoh sebelum: satu kontrak generik yang terlihat rapi tetapi rapuh
Berikut contoh desain yang sering muncul di awal proyek:
POST /integration/handle
Headers:
X-Auth: <token-or-signature>
Body:
{
"action": "login|refresh|webhook",
"username": "optional",
"password": "optional",
"refresh_token": "optional",
"event_type": "optional",
"event_id": "optional",
"payload": {},
"idempotency_key": "optional",
"retry_count": 0
}
Response:
200 OK
{
"success": true,
"code": "OK|INVALID_AUTH|RETRY_LATER",
"data": {}
}Di atas kertas ini tampak konsisten. Dalam praktik, masalahnya besar:
- Satu endpoint menanggung terlalu banyak mode operasi.
- Dokumentasi validasi menjadi panjang dan conditional.
- Monitoring sulit karena semua trafik masuk ke route yang sama.
- Keamanan melemah karena satu header memegang banyak arti.
- Penggunaan status code HTTP menjadi tidak informatif.
- Idempotency dan retry bercampur tanpa batas yang jelas.
Contoh sesudah: kontrak spesifik per alur
Pemisahan yang lebih aman biasanya dimulai dari perilaku, bukan dari keinginan menyeragamkan bentuk JSON.
Auth endpoint terpisah
POST /auth/login
Authorization: (tidak diperlukan)
Content-Type: application/json
{
"username": "alice",
"password": "secret"
}
200 OK
{
"access_token": "...",
"expires_in": 3600
}POST /auth/refresh
Content-Type: application/json
{
"refresh_token": "..."
}
200 OK
{
"access_token": "...",
"expires_in": 3600
}Kedua endpoint ini serupa, tetapi belum tentu perlu disatukan jika aturan validasi, audit, rate limit, atau error semantics-nya berbeda.
Webhook endpoint terpisah
POST /webhooks/provider-a
X-Signature: sha256=...
X-Event-Id: evt_123
X-Event-Type: invoice.paid
Content-Type: application/json
{ ... raw event payload ... }Responsenya dibuat spesifik terhadap delivery semantics:
- 2xx jika event diterima dan tidak perlu diulang.
- 4xx jika request salah secara permanen, misalnya signature tidak valid.
- 5xx jika kegagalan sementara dan pengirim boleh mencoba lagi.
Jika pemrosesan bisnis berat, simpan event ke queue internal lalu cepat balas 202 atau 200, sesuai kontrak yang Anda publikasikan. Kuncinya: acknowledgement delivery dan processing business logic adalah dua tahap berbeda.
Idempotency dibuat eksplisit
Untuk API yang dipanggil klien agar membuat resource, dukungan idempotency bisa seperti ini:
POST /payments
Idempotency-Key: 6e7b4f...
Content-Type: application/json
{
"amount": 100000,
"currency": "IDR",
"reference": "INV-123"
}Server menyimpan hasil berdasarkan kombinasi identitas klien, idempotency key, dan representasi request yang relevan. Ini berbeda dari webhook inbound, yang lebih cocok memakai event_id atau identifier delivery dari provider untuk deduplikasi.
Kapan duplikasi kecil lebih aman
Duplikasi bukan selalu masalah. Dalam konteks kontrak API, duplikasi kecil justru membantu menjaga batas perilaku.
Lebih aman membiarkan dua endpoint terlihat mirip tetapi terpisah jika:
- Aturan validasi berbeda.
- Model keamanan berbeda.
- Kebijakan retry berbeda.
- Makna status code berbeda.
- Kebutuhan observability dan audit berbeda.
Contoh: /auth/login dan /auth/refresh sama-sama mengembalikan access token. Namun menyatukannya ke /auth/action sering menambah branch conditional yang tidak perlu. Duplikasi response token kecil tidak masalah jika sebagai gantinya kontrak tetap jelas.
Jika Anda mulai menambah field opsional, flag mode, dan pengecualian dokumentasi untuk mempertahankan satu abstraksi, itu tanda abstraksi tersebut perlu dipecah, bukan ditambal.
Pola implementasi yang lebih tahan perubahan
Pisahkan jalur transport, verifikasi, dan domain processing
Untuk webhook khususnya, jangan campur semua langkah dalam satu handler besar. Pecah menjadi tahap yang eksplisit:
- Terima request dan simpan konteks dasar: header, body mentah, request ID.
- Verifikasi signature terhadap body mentah.
- Validasi event envelope: event ID, event type, timestamp jika ada.
- Lakukan deduplikasi event.
- Kirim event ke queue atau proses sinkron jika ringan.
- Catat hasil acknowledgment terpisah dari hasil business processing.
Pemisahan ini memudahkan debugging: Anda bisa tahu apakah event gagal di verifikasi, di deduplikasi, atau di logic bisnis.
Gunakan schema per endpoint, bukan schema super-set
Daripada satu schema besar yang berisi semua field opsional, definisikan schema masing-masing endpoint atau event type. Bila beberapa struktur benar-benar sama, ekstrak komponen kecil yang stabil, misalnya pagination, error object, atau token response, bukan seluruh kontrak lintas-alur.
Bedakan retry eksternal dan retry internal
Retry pengirim webhook, retry HTTP client, dan retry job queue adalah tiga hal berbeda. Dokumentasikan dan implementasikan secara terpisah:
- Retry eksternal: dipicu status code atau timeout dari penerima webhook.
- Retry internal: dipicu queue worker saat proses domain gagal sementara.
- Retry klien: dipakai pada API klien tertentu dan biasanya dibatasi oleh idempotency.
Dengan pemisahan ini, Anda tidak perlu menaruh field generik seperti retry_count dalam kontrak publik kecuali memang ada alasan nyata dan terdokumentasi.
Checklist refactor bertahap dari kontrak terlalu umum
Jika sistem Anda sudah terlanjur memakai satu kontrak generik, refactor tidak perlu dilakukan sekaligus. Pendekatan aman biasanya bertahap:
- Petakan variasi perilaku nyata
Daftar semua mode yang saat ini ditopang kontrak generik: login, refresh, webhook inbound, callback internal, create resource idempotent, dan sebagainya. - Identifikasi field yang kondisional
Tandai field yang hanya berlaku di satu mode. Jika jumlahnya banyak, itu kandidat pemisahan endpoint atau schema. - Pisahkan header berdasarkan makna
Buat nama header yang spesifik untuk auth, signature, request ID, dan idempotency. - Perbaiki status code lebih dulu
Sering kali ini perubahan dengan dampak besar dan implementasi relatif jelas. Pastikan klien tahu arti 2xx, 4xx, dan 5xx per endpoint. - Buat endpoint baru berdampingan
Jangan langsung mengubah endpoint lama jika sudah dipakai integrator. Tambahkan route baru yang lebih spesifik, lalu migrasikan klien bertahap. - Tambahkan adapter sementara
Biarkan endpoint lama menerjemahkan request lama ke handler baru di internal. Ini mengurangi duplikasi bisnis saat masa transisi. - Ukur penggunaan endpoint lama
Gunakan observability untuk melihat siapa yang masih memakai kontrak lama sebelum deprecate. - Hapus abstraksi lama setelah benar-benar sepi
Jangan simpan dua model selamanya jika salah satunya sudah tidak dipakai.
Contract testing agar perubahan tetap aman
Saat memecah kontrak generik, risiko terbesar adalah mematahkan integrasi yang sudah jalan. Karena itu, contract testing sangat membantu.
Apa yang perlu diuji
- Request contract: field wajib, tipe data, header yang harus ada, format signature, dan batas validasi.
- Response contract: status code, struktur body, error shape, dan header response penting.
- Behavior contract: apakah duplicate webhook di-ack dengan aman, apakah request dengan idempotency key sama menghasilkan efek yang sama, apakah invalid auth menghasilkan kode yang tepat.
Pendekatan praktis
Anda tidak harus memakai tool tertentu untuk mendapat manfaat. Minimal lakukan:
- Spesifikasi kontrak yang eksplisit, misalnya dalam OpenAPI atau format setara.
- Test otomatis yang memverifikasi implementasi server terhadap spesifikasi tersebut.
- Jika ada klien internal, buat consumer-driven contract untuk skenario yang paling kritis.
Untuk webhook, tambahkan fixture body mentah dan signature yang valid/invalid. Ini penting karena bug verifikasi sering muncul bukan pada JSON field, tetapi pada cara body dibaca dan dihitung.
Versioning: ubah kontrak tanpa membuat semua klien panik
Memecah abstraksi salah sering berujung pada perubahan kontrak publik. Lakukan versioning secara disiplin.
Kapan perlu versi baru
- Mengubah bentuk request yang inkompatibel.
- Mengubah arti status code yang selama ini dipakai klien.
- Mengganti mekanisme auth atau signature.
- Menghapus field yang masih digunakan klien.
Prinsip aman
- Tambahkan endpoint atau versi baru sebelum menghapus yang lama.
- Berikan periode transisi yang jelas.
- Log pemakaian kontrak lama per klien atau per credential.
- Dokumentasikan perbedaan semantik, bukan hanya beda field.
Versioning tidak selalu harus berarti /v2 di URL. Yang penting adalah strategi kompatibilitasnya konsisten dan dapat dipantau. Namun apa pun bentuknya, jangan mencampur dua semantik besar ke endpoint yang sama tanpa penanda yang jelas.
Observability yang membantu mendeteksi abstraction leak
Abstraksi yang salah sering baru terasa saat operasi berjalan: retry membengkak, webhook sering duplikat, atau error auth susah dipisahkan dari invalid signature. Karena itu, observability harus dirancang mengikuti batas kontrak yang benar.
Log yang sebaiknya ada
- Request ID unik per permintaan.
- Event ID / delivery ID untuk webhook.
- Idempotency key untuk mutasi dari klien.
- Auth subject atau identitas principal jika relevan, tanpa membocorkan secret.
- Decision log: signature valid/tidak, dedupe hit/tidak, enqueue sukses/tidak, status akhir response.
Metrics penting
- Jumlah request per endpoint dan per status code.
- Tingkat invalid signature pada webhook.
- Rasio duplicate event yang tertangani.
- Latency acknowledgment webhook terpisah dari latency processing internal.
- Jumlah retry internal queue dan penyebabnya.
Kalau semua mode masih digabung dalam satu endpoint generik, metrics ini sulit dibaca. Itu alasan lain mengapa pemisahan kontrak memberi keuntungan operasional nyata, bukan sekadar kerapian desain.
Kesalahan umum saat merapikan desain
- Terlalu cepat membuat base handler universal
Boleh berbagi utilitas kecil, tetapi jangan paksa satu alur hidup untuk semua endpoint. - Menyamakan format error dengan menyamakan semantik
Anda boleh punya struktur error seragam, tetapi status code dan maknanya tetap harus sesuai konteks. - Mencampur idempotency dengan deduplikasi webhook
Keduanya mirip, tetapi sumber identitas dan tanggung jawabnya berbeda. - Memverifikasi webhook setelah parsing yang mengubah body
Gunakan body mentah untuk perhitungan signature. - Menganggap duplikasi kode kecil pasti buruk
Duplikasi kecil sering menjadi pagar yang menjaga batas domain tetap jelas.
Penutup
Hindari abstraksi dini pada API auth dan webhook dengan memulai dari perilaku yang benar, bukan dari bentuk kontrak yang seragam. Jika auth, webhook, retry, dan idempotency memiliki aturan operasi yang berbeda, biarkan kontraknya juga berbeda. Sedikit duplikasi pada endpoint, schema, atau handler sering lebih murah daripada satu abstraksi besar yang penuh field opsional, header ambigu, dan semantik yang bocor ke mana-mana.
Praktik yang paling bisa diterapkan adalah ini: pisahkan endpoint berdasarkan trust model dan delivery model, gunakan status code yang jujur, buat idempotency eksplisit, verifikasi webhook dari body mentah, lalu lindungi perubahan dengan contract testing, versioning, dan observability. Dengan begitu, integrasi lebih mudah dipahami, lebih aman diubah, dan lebih mudah di-debug saat masalah nyata muncul.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!