Merancang API sebagai kontrak yang dijaga: prinsip REST yang benar, strategi versioning dan deprecation tanpa mematahkan konsumen, perbandingan REST vs gRPC vs GraphQL, idempotency untuk operasi uang, serta contract testing — dipraktikkan pada spesifikasi OpenAPI Order API

Setelah di episode 8 kita merancang arsitektur data — kepemilikan per modul, spektrum konsistensi, dan outbox pattern — pada episode ini kita bahas permukaan tempat semua desain itu bertemu dunia luar: API.
Bagi architect, API bukan sekadar endpoint; ia adalah kontrak — janji perilaku yang, begitu dirilis, jauh lebih mahal diubah daripada kode internal. Modul bisa direfactor setiap minggu tanpa siapa pun keberatan; mengganti nama satu field publik bisa mematahkan puluhan konsumen dan menuntut koordinasi lintas tim selama berbulan-bulan. Karena itu kualitas desain API adalah indikator langsung kematangan arsitektur.
REST yang baik dimodelkan dari resource (benda), bukan dari aksi (verb). Bandingkan:
❌ POST /createOrder
❌ POST /cancelOrder?id=123
✅ POST /orders
✅ PATCH /orders/123 { "status": "cancelled" }Resource-oriented membuat sistem predictable: konsumen menebak endpoint baru dengan benar sebelum membaca dokumentasi.
Status code adalah bagian dari kontrak:
| Kode | Makna Kontraktual | Kesalahan Umum |
|---|---|---|
| 200 | Sukses dengan body sesuai verb | Mengembalikan {"success": false} di dalam 200 |
| 201 | Resource baru tercipta + header Location | Membuat data tapi tetap 200 |
| 400 | Request salah bentuk (kesalahan klien) | Menyembunyikan bug server di sini |
| 401 vs 403 | Belum terautentikasi vs tidak berizin | Tertukar |
| 422 | Validasi semantik gagal | Dipakai acak bersama 400 |
| 409 | Konflik state (idempotency replay) | Dilempar generik |
| 500 | Bug server | Menjadi tempat sampah semua error |
Error body mengikuti struktur konsisten — format RFC 7807 (application/problem+json) adalah pilihan aman: type, title, status, detail, plus field ekstensi seperti traceId agar error mudah dilacak.
Koleksi besar wajib paginated sejak hari pertama — retrofit pagination ke API publik adalah breaking change terselubung. Offset pagination (?page=2&limit=20) mudah tapi melorot saat data bergeser; cursor pagination stabil untuk feed besar. Cantumkan aturan default limit dan batas maksimumnya di kontrak.
Aturan emas: menambah adalah aman, menghapus/mengubah adalah breaking.
AMAN → tambah field respons opsional, tambah endpoint baru,
tambah parameter opsional dengan default
BREAKING→ hapus/renamed field, ubah tipe, ubah semantik status,
jadikan optional menjadi requiredTiga strategi versioning:
| Strategi | Contoh | Trade-off |
|---|---|---|
| URL path | /v2/orders | Paling eksplisit, routing mudah, paling umum |
| Header custom | X-API-Version: 2026-08-16 | URL bersih, tapi tak terlihat di browser/log kasual |
| Media type | Accept: application/vnd.toko.v2+json | Paling "murni", jarang dipakai praktis |
Untuk API internal antar tim, versi date-based (2026-08-16) semakin populer: memaksa evolusi berkala dan menghindari angka v7 yang menyeramkan.
Deprecation juga kontrak. Aturannya: umumkan → tandai response header peringatan (Deprecation/Sunset) → pantau siapa masih memakai → matikan hanya setelah konsumen nol atau tenggat lewat. Versi lama yang mati mendadak adalah insiden yang kalian buat sendiri.
Important
Untuk API internal monolith kita (antar modul), versioning penuh biasanya overkill — cukup disiplin backward-compatible change. Simpan versioning formal untuk API yang dikonsumsi pihak luar, karena di sanalah biaya koordinasi breaking change paling mahal.
| Dimensi | REST | gRPC | GraphQL |
|---|---|---|---|
| Model | Resource + HTTP verbs | RPC via protobuf | Query graph dari client |
| Kontrak | OpenAPI (opsional) | .proto (wajib) | Schema (wajib) |
| Performa | HTTP/JSON standar | HTTP/2 binary, sangat cepat | Tergantung resolver |
| Cocok untuk | API publik, integrasi umum | Komunikasi internal latency-rendah | BFF mobile/web, aggregasi |
| Caching | HTTP caching alami | Perlu manual | Sulit, butuh tooling |
| Learning curve | Rendah | Sedang | Sedang-tinggi |
Panduan praktis: REST untuk permukaan publik, gRPC untuk komunikasi internal latency-kritis, GraphQL untuk layer agregasi di depan banyak konsumen frontend. Studi kasus kita pakai REST publik + panggilan internal in-process (modular monolith), sehingga kompleksitas gRPC belum dibayar.
Satu bahaya GraphQL layak dicatat bagi architect: query bebas tanpa governance menghasilkan N+1 resolver dan query monster yang menjatuhkan server — wajib disertai complexity limiting dan persisted queries.
Retry pasti terjadi (jaringan timeout, user klik dua kali). Operasi non-idempotent seperti POST /orders akan menciptakan order dobel — persis scenario correctness episode 5 (double-charge = 0).
Solusinya idempotency key:
1. Client mengirim header Idempotency-Key: <uuid> per intent transaksi
2. Server menyimpan hasil pertama keyed by uuid
3. Retry dengan key sama → kembalikan HASIL PERTAMA, bukan eksekusi ulang
4. Key beda → dieksekusi normalIni pola yang sama dengan charge gateway pembayaran — dan wajib ada di kontrak API apa pun yang memicu efek finansial atau mutasi stok.
Dua pendekatan kontrak:
Untuk API publik, contract first hampir selalu menang karena diskusi desain terjadi sebelum kode mengunci keputusan.
Lalu contract testing memastikan janji itu dijaga otomatis: consumer-driven testing (misal Pact) merekam ekspektasi konsumen terhadap provider, dan pipeline provider memverifikasi setiap perubahan terhadap semua kontrak konsumen. Breaking change tertangkap di CI, bukan di produksi konsumen.
Mari tulis potongan kontrak utama studi kasus kita:
openapi: "3.1.0"
info:
title: Orders API
version: "2026-08-16"
paths:
/orders:
post:
summary: Buat order baru
security: [{ bearerAuth: [] }]
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [items]
properties:
items:
type: array
minItems: 1
items:
$ref: "#/components/schemas/OrderItemInput"
responses:
"201":
description: Order dibuat, menunggu pembayaran
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"409":
description: Stok habis / order duplikat (replay)
"422":
description: Validasi item gagal
components:
parameters:
IdempotencyKey:
name: Idempotency-Key
in: header
required: true
schema: { type: string, format: uuid }Catat tiga keputusan desain yang terkandung di dalamnya: idempotency wajib di jalur checkout (episode 8), 409 eksplisit untuk konflik stok/replay, dan 201 dengan schema Order penuh sebagai janji bentuk respons. Kontrak ini kini bisa digenerate ke stub server agar tim frontend mulai bekerja paralel sebelum backend selesai.
Inti yang harus dibawa pulang:
Di episode 10 selanjutnya kita masuk security architecture — threat modeling dengan STRIDE terhadap flow checkout kita, prinsip secure design, defense in depth, dan fondasi zero trust yang akan diperdalam di episode 18. Sampai jumpa!