Masalah integrasi API jarang muncul karena endpoint benar-benar tidak ada. Biasanya masalah muncul karena kontrak tidak ditulis cukup jelas: field mana wajib, error seperti apa yang stabil, apakah request aman di-retry, bagaimana verifikasi webhook, dan perubahan apa yang masih kompatibel. Engineer sering lebih mudah mengkritik desain yang sudah ada daripada merancang dari nol. Kabar baiknya, insting review itu bisa diubah menjadi checklist kontrak API yang sistematis.

Artikel ini fokus pada cara menulis dan me-review kontrak API sebelum tim backend, frontend, mobile, atau partner eksternal mulai integrasi. Tujuannya bukan membuat dokumen tebal, tetapi memastikan ada keputusan eksplisit untuk hal-hal yang paling sering memicu bug produksi.

Mengapa checklist kontrak API lebih berguna daripada spesifikasi yang terlalu umum

Dokumen API sering terlihat lengkap karena berisi daftar endpoint, contoh payload, dan status code. Namun integrasi tetap gagal jika pertanyaan penting tidak terjawab. Contoh:

  • Apakah field boleh hilang, atau hanya boleh bernilai null?
  • Jika client timeout, apakah aman mengirim ulang request yang sama?
  • Apakah format error konsisten di semua endpoint?
  • Jika provider mengirim webhook dua kali, bagaimana sistem penerima harus bereaksi?
  • Perubahan apa yang dianggap breaking?

Checklist memaksa tim menjawab pertanyaan review yang biasanya baru muncul saat QA atau produksi. Ini penting karena memperbaiki kontrak setelah beberapa client terhubung jauh lebih mahal daripada mendefinisikannya di awal.

Checklist kontrak API sebelum integrasi dimulai

Bagian ini bisa langsung dipakai sebagai template review. Jika satu item belum terjawab, anggap kontrak belum siap untuk diintegrasikan.

1. Tujuan endpoint dan operasi bisnisnya jelas

Sebelum membahas field, pastikan satu endpoint mewakili satu operasi yang jelas. Misalnya, bedakan antara:

  • POST /payments untuk membuat pembayaran baru
  • POST /payments/{id}/capture untuk menangkap otorisasi pembayaran
  • POST /payments/{id}/cancel untuk membatalkan pembayaran

Kesalahan umum adalah membuat endpoint generik dengan banyak perilaku tersembunyi di satu payload. Sulit direview, sulit diuji, dan rawan perubahan diam-diam.

2. Field wajib vs opsional harus eksplisit

Ini salah satu sumber bug integrasi terbesar. Jangan hanya menulis contoh JSON. Tuliskan untuk setiap field:

  • Apakah wajib saat create?
  • Apakah opsional saat update?
  • Apakah boleh null?
  • Apakah boleh string kosong?
  • Apakah server memberi default jika field tidak dikirim?
  • Apakah field hanya output, hanya input, atau keduanya?

Aturan praktis: bedakan tiga keadaan yang sering tercampur:

  • Tidak dikirim: client tidak mengirim field sama sekali
  • Null: client mengirim field dengan nilai null
  • Kosong: client mengirim string kosong atau array kosong

Ketiganya bisa punya arti berbeda, dan kontrak harus menyatakannya.

Contoh buruk vs baik untuk field

Buruk: dokumentasi hanya menyebut “email opsional”.

POST /users
{
  "name": "Rina",
  "email": null
}

Masalahnya:

  • Apakah email boleh null atau harus dihilangkan jika tidak ada?
  • Apakah email unik?
  • Apakah string kosong valid?
  • Apakah field ini wajib pada alur tertentu?

Baik: kontrak mendefinisikan perilaku yang bisa diimplementasikan dan diuji.

POST /users
{
  "name": "Rina",
  "email": "[email protected]"
}
  • name: wajib, string non-kosong, panjang maksimum ditentukan oleh server
  • email: opsional saat create
  • Jika email tidak dikirim, server membuat user tanpa email
  • Jika email dikirim, nilainya harus format email valid
  • email: null tidak valid
  • email: "" tidak valid
  • Field id, created_at hanya output dari server

Kontrak seperti ini membuat validasi, testing, dan integrasi lebih konsisten.

3. Model error harus konsisten dan dapat diproses mesin

Client tidak cukup hanya tahu bahwa request gagal. Client perlu tahu apakah error bisa diperbaiki, di-retry, atau harus dilaporkan ke operator.

Minimal, model error yang baik punya:

  • Status HTTP yang sesuai
  • Kode error stabil untuk logika aplikasi
  • Pesan manusia yang bisa dibaca
  • Detail field-level jika validasi gagal
  • Request identifier atau trace identifier untuk debugging

Contoh buruk:

HTTP/1.1 400 Bad Request
{
  "message": "Something went wrong"
}

Pesan seperti ini tidak membantu client menentukan tindakan.

Contoh lebih baik:

HTTP/1.1 422 Unprocessable Entity
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Payload tidak valid",
    "details": [
      {
        "field": "email",
        "issue": "invalid_format"
      }
    ],
    "request_id": "req_7f3a1c"
  }
}

Mengapa ini bekerja:

  • code stabil untuk percabangan di client
  • message bisa dipakai untuk log atau UI internal
  • details membantu form validation atau debugging cepat
  • request_id memudahkan pencarian log antar sistem

Hindari menjadikan teks message sebagai kontrak utama. Teks mudah berubah. Gunakan code sebagai identifier yang stabil.

4. Idempotency key untuk operasi yang berisiko duplikat

Untuk operasi seperti membuat pembayaran, order, refund, atau booking, timeout jaringan bisa membuat client ragu apakah request pertama berhasil. Tanpa mekanisme idempotensi, retry dapat menciptakan data ganda.

Pola yang umum dan aman:

  • Client mengirim Idempotency-Key unik per operasi bisnis
  • Server menyimpan hasil pertama untuk kombinasi kunci dan konteks yang relevan
  • Jika request yang sama datang lagi, server mengembalikan hasil yang sama, bukan membuat resource baru

Contoh request:

POST /payments
Idempotency-Key: 6d8c1f8a-5f7b-4d23-8f4e-9c3ab21f7a11
Content-Type: application/json

{
  "order_id": "ord_123",
  "amount": 150000,
  "currency": "IDR"
}

Yang perlu ditulis di kontrak:

  • Endpoint mana yang mendukung idempotency key
  • Berapa lama key dianggap aktif
  • Apakah payload harus identik untuk key yang sama
  • Respons apa yang dikembalikan jika key yang sama dipakai dengan payload berbeda

Kesalahan umum: hanya menambahkan header idempotency tanpa mendefinisikan perilakunya. Hasilnya, client merasa aman melakukan retry padahal server belum benar-benar idempotent.

5. Retry safety harus didefinisikan, bukan diasumsikan

Tidak semua request aman diulang. Tim integrasi perlu tahu dengan jelas:

  • Operasi mana yang aman di-retry otomatis
  • Kondisi retry yang direkomendasikan, misalnya timeout atau respons 5xx
  • Apakah client harus memakai backoff
  • Apakah ada batas retry untuk mencegah lonjakan trafik

Panduan praktis:

  • GET biasanya aman di-retry jika bersifat read-only
  • POST tidak otomatis aman, kecuali ada kontrak idempotensi yang jelas
  • PUT dan DELETE bisa lebih aman di-retry jika semantiknya benar-benar idempotent, tetapi tetap perlu definisi respons dan efek samping

Jika operasi memicu side effect ke sistem lain, nyatakan itu. Contoh: “request mungkin sudah diproses walau client menerima timeout.” Kalimat ini penting untuk strategi recovery di sisi client.

6. Timeout, SLA teknis, dan ekspektasi latensi

Kontrak API tidak harus memuat janji performa yang terlalu spesifik, tetapi minimal harus menjelaskan batas operasional dasar:

  • Apakah endpoint ini sinkron atau asinkron?
  • Apakah client disarankan timeout pendek atau menunggu lebih lama?
  • Jika proses lama, apakah lebih baik balas 202 Accepted lalu sediakan endpoint status?

Contoh buruk: endpoint ekspor data menjalankan proses berat secara sinkron tanpa penjelasan, lalu client memasang timeout 5 detik dan mulai me-retry agresif.

Contoh lebih baik:

  • POST /exports mengembalikan 202 Accepted
  • Respons berisi job_id
  • Client memeriksa GET /exports/{job_id}
  • Retry pada create ekspor memakai idempotency key

Pola ini mengurangi timeout palsu dan mencegah pekerjaan ganda.

7. Webhook signature dan replay protection

Jika integrasi melibatkan webhook, kontrak harus menjawab dua pertanyaan: bagaimana penerima yakin payload benar-benar dari pengirim, dan bagaimana mencegah replay request lama.

Minimal, definisikan:

  • Header signature yang dikirim
  • Algoritma atau mekanisme verifikasi secara umum
  • Apakah signature dihitung dari raw body
  • Apakah ada timestamp untuk membatasi replay
  • Respons apa yang dianggap sukses oleh pengirim
  • Kebijakan retry jika endpoint penerima gagal

Contoh kontrak yang lebih aman:

  • Pengirim mengirim header X-Signature dan X-Timestamp
  • Penerima menghitung ulang signature dari raw request body dan secret bersama
  • Penerima menolak request jika timestamp terlalu lama atau signature tidak cocok
  • Penerima harus memproses event secara idempotent berdasarkan event_id

Kesalahan umum:

  • Memverifikasi payload JSON yang sudah di-parse ulang, padahal signature dihitung dari body mentah
  • Tidak menyimpan event_id sehingga event duplikat diproses dua kali
  • Menganggap sekali kirim pasti cukup, padahal webhook hampir selalu memerlukan retry

8. Versioning dan strategi perubahan

Versioning bukan sekadar menambahkan /v1 di URL. Tujuan utamanya adalah mengelola perubahan tanpa merusak client yang sudah ada.

Yang perlu diputuskan:

  • Di mana versi dinyatakan: URL, header, atau media type
  • Perubahan apa yang dianggap breaking
  • Bagaimana masa transisi dan deprecasi diumumkan
  • Apakah tim berkomitmen menjaga kompatibilitas ke belakang dalam satu versi mayor

Perubahan yang umumnya aman jika client ditulis defensif:

  • Menambah field baru di respons
  • Menambah endpoint baru
  • Menambah kode error baru yang terdokumentasi dengan baik

Perubahan yang umumnya breaking:

  • Menghapus field respons
  • Mengubah tipe data field
  • Mengubah makna field yang sudah ada
  • Mengubah status code sukses utama tanpa transisi
  • Menjadikan field yang dulu opsional menjadi wajib tanpa fallback

Jika API Anda dikonsumsi banyak klien, asumsi paling aman adalah: perubahan kecil di server tetap bisa menjadi perubahan besar di client.

9. Kompatibilitas perubahan harus diuji sebagai kontrak, bukan hanya unit test

Banyak tim punya test logika bisnis, tetapi tidak punya test kompatibilitas kontrak. Akibatnya, perubahan serializer, validator, atau mapping DTO bisa lolos ke produksi dan memutus integrasi.

Praktik yang berguna:

  • Simpan contoh request/response kanonik sebagai artefak kontrak
  • Tambahkan schema validation untuk payload penting
  • Jalankan contract test antara provider dan consumer jika memungkinkan
  • Pastikan perubahan pada field, error code, dan header ikut direview

Ini sangat membantu untuk mencegah bug “secara teknis endpoint masih hidup, tetapi kontraknya berubah.”

Tabel checklist review kontrak API

AreaPertanyaan reviewTanda bahaya
Tujuan endpointApakah operasi bisnisnya satu dan jelas?Satu endpoint menangani terlalu banyak skenario tersembunyi
FieldApakah wajib, opsional, nullability, default, dan read/write sudah jelas?Hanya ada contoh JSON tanpa aturan validasi
Error modelApakah ada error code stabil, detail validasi, dan request ID?Semua gagal dibalas dengan pesan generik
IdempotencyApakah operasi create/capture/refund aman dari duplikasi?Retry bisa membuat resource ganda
Retry safetyKapan client boleh retry, dan pada status apa?Tim berasumsi semua timeout aman diulang
TimeoutApakah alur sinkron/asinkron dan ekspektasi respons jelas?Operasi lama dipaksa sinkron tanpa polling/job status
WebhookBagaimana verifikasi signature, timestamp, dan deduplikasi event?Webhook hanya mengandalkan IP atau tanpa replay protection
VersioningBagaimana strategi deprecasi dan definisi breaking change?Perubahan kontrak dilakukan tanpa jalur transisi
KompatibilitasApakah ada contract test atau schema check?Hanya unit test internal provider
ObservabilityApakah ada request ID, event ID, atau correlation ID?Sulit menelusuri masalah lintas service

Contoh kontrak buruk vs baik untuk operasi create yang sering timeout

Contoh buruk

POST /orders
{
  "customer_id": "cust_1",
  "items": [
    { "sku": "A-1", "qty": 2 }
  ]
}

HTTP/1.1 200 OK
{
  "message": "success"
}

Masalah desain:

  • Tidak jelas field mana wajib dan batas validasinya
  • Tidak ada order_id yang dapat direferensikan
  • Tidak ada idempotency key untuk mencegah order ganda
  • Tidak ada model error untuk validasi atau stok habis
  • Status 200 OK terlalu umum untuk create

Contoh lebih baik

POST /orders
Idempotency-Key: 17d1e7c9-1f84-4e63-8f03-9e7d6f1d1b6b
Content-Type: application/json

{
  "customer_id": "cust_1",
  "items": [
    { "sku": "A-1", "qty": 2 }
  ],
  "notes": "kirim sore"
}

HTTP/1.1 201 Created
{
  "id": "ord_987",
  "status": "pending",
  "customer_id": "cust_1",
  "items": [
    { "sku": "A-1", "qty": 2 }
  ],
  "notes": "kirim sore",
  "created_at": "2026-07-24T10:15:30Z"
}

Aturan kontrak yang melengkapinya:

  • customer_id wajib
  • items wajib, minimal satu item
  • notes opsional, jika tidak dikirim server menyimpan kosong
  • Idempotency-Key wajib untuk create order dari client eksternal
  • Jika request sama diulang dengan key sama, server mengembalikan order yang sama
  • Jika key sama tetapi payload berbeda, server mengembalikan error konflik

Contoh error konflik idempotensi:

HTTP/1.1 409 Conflict
{
  "error": {
    "code": "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD",
    "message": "Idempotency key sudah dipakai untuk payload lain",
    "request_id": "req_a12bc3"
  }
}

Kontrak ini lebih aman karena perilaku timeout dan retry tidak diserahkan ke tebakan client.

Langkah verifikasi saat bekerja lintas tim

Kontrak API yang baik tidak cukup ditulis oleh satu tim lalu dilempar ke tim lain. Verifikasi lintas tim penting agar asumsi yang tersembunyi muncul sebelum coding dimulai.

1. Review dengan skenario, bukan hanya endpoint

Jangan bertanya “apakah endpoint ini sudah oke?” Tanyakan skenario konkret:

  • Apa yang terjadi jika client kirim field opsional sebagai null?
  • Apa respons saat timeout setelah server sempat memproses request?
  • Bagaimana jika webhook terkirim dua kali?
  • Apa yang harus dilakukan client untuk error validasi vs error sementara?

Skenario seperti ini mengubah review dari diskusi abstrak menjadi keputusan kontrak yang bisa diuji.

2. Minta contoh sukses dan gagal yang kanonik

Untuk setiap endpoint penting, siapkan minimal:

  • Satu contoh request sukses
  • Satu contoh error validasi
  • Satu contoh error bisnis
  • Satu contoh kasus retry atau idempotensi jika relevan

Jika tim tidak bisa menulis contoh gagal dengan jelas, biasanya kontraknya memang belum cukup matang.

3. Samakan istilah bisnis dan teknis

Sering kali dua tim memakai kata yang sama dengan arti berbeda, misalnya “cancel”, “void”, “expired”, atau “failed”. Pastikan status dan transisinya punya arti tunggal. Ini lebih penting daripada sekadar nama field yang rapi.

4. Putuskan pemilik kontrak

Satu tim harus bertanggung jawab menjaga kontrak tetap konsisten. Tanpa pemilik yang jelas, perubahan kecil bisa masuk dari banyak arah: backend mengubah serializer, frontend mengandalkan field tak terdokumentasi, atau partner menyimpan asumsi dari percakapan chat.

5. Tambahkan checklist ke pull request atau design review

Supaya kebiasaan review menjadi bagian dari proses, masukkan butir checklist ini ke template PR, RFC, atau design doc. Contohnya:

  • Apakah field baru breaking untuk consumer lama?
  • Apakah retry behavior sudah dijelaskan?
  • Apakah error code baru sudah terdokumentasi?
  • Apakah webhook event idempotent?

Tujuannya bukan menambah birokrasi, tetapi menangkap masalah kontrak di saat biaya perubahan masih murah.

Kesalahan yang paling sering terjadi

  • Menganggap contoh payload sama dengan spesifikasi. Contoh hanya ilustrasi, bukan aturan lengkap.
  • Menyatukan field opsional dan nullable. Padahal perilakunya bisa sangat berbeda.
  • Berfokus pada endpoint sukses saja. Padahal integrasi lebih banyak gagal di jalur error.
  • Tidak mendefinisikan idempotensi untuk create. Ini sering berujung pada duplikasi data saat gangguan jaringan.
  • Menambahkan field respons tanpa memikirkan consumer yang ketat. Secara teori aman, tetapi beberapa parser atau mapping internal bisa rapuh.
  • Webhook tanpa deduplikasi event. Hasilnya side effect dijalankan berkali-kali.

Penutup

Checklist kontrak API membantu mengubah kemampuan review yang sudah dimiliki banyak engineer menjadi alat desain yang konkret. Sebelum integrasi dimulai, pastikan kontrak menjawab hal-hal yang paling berbahaya: field wajib vs opsional, model error, idempotency key, retry safety, webhook signature, timeout, versioning, dan kompatibilitas perubahan.

Jika tim Anda sering berkata “nanti kita lihat saat implementasi”, biasanya itu tanda kontraknya belum siap. Lebih aman memaksa jawaban sejak review awal daripada membiarkan client dan server menebak perilaku satu sama lain di produksi.