Kebanyakan API REST gagal bukan karena framework-nya salah, tapi karena dipikirkan sebagai kumpulan endpoint alih-alih sebuah kontrak jangka panjang. Developer terburu-buru mengekspos tabel database sebagai resource, memakai kata kerja di URL, dan baru sadar desainnya rapuh ketika konsumen pertama datang.

Artikel ini membahas cara membangun REST API web service yang layak dipakai dalam jangka panjang: semantik HTTP yang benar, resource modeling, versioning, pagination, error handling, autentikasi, dan keputusan kapan REST bukan pilihan yang tepat.

REST Bukan Sekadar JSON di Atas HTTP

Ada kesalahpahaman umum bahwa REST berarti "kirim JSON lewat HTTP". Sebenarnya REST adalah seperangkat batasan arsitektur (architectural constraints) yang diperkenalkan Roy Fielding: client-server, stateless, cacheable, uniform interface, layered system, dan code-on-demand.

Dua hal yang paling sering diabaikan di implementasi nyata:

  1. Stateless — server tidak boleh menyimpan state sesi klien. Setiap request harus membawa semua informasi yang dibutuhkan untuk diproses. Kalau API kamu bergantung pada "sesi login di server", itu melanggar prinsip ini dan menyulitkan horizontal scaling.
  2. Uniform interface — resource diidentifikasi oleh URL, dimanipulasi lewat representasi, dan punya pesan yang self-descriptive. Konsumen tidak boleh perlu "tahu" kebiasaan khusus server di luar kontrak.

Poin penting: banyak "REST API" di dunia nyata sebenarnya adalah HTTP JSON API yang pragmatis. Tidak apa-apa — yang berbahaya adalah mengklaim REST penuh lalu melanggar semantik HTTP secara diam-diam.

Resource Modeling: Kata Benda, Bukan Kata Kerja

Langkah pertama yang benar adalah memodelkan domain sebagai resource, bukan sebagai aksi.

Buruk:

POST /api/getUserById
POST /api/createOrder
POST /api/deleteProduct

Baik:

GET    /api/users/{id}
POST   /api/orders
DELETE /api/products/{id}

Resource adalah kata benda jamak. Aksi diwakili oleh HTTP method yang sudah punya semantik jelas:

  • GET — baca, idempotent, aman
  • POST — buat resource baru
  • PUT — ganti resource secara utuh (idempotent)
  • PATCH — ubah sebagian
  • DELETE — hapus

Perbedaan PUT vs PATCH sering membingungkan. PUT bersifat idempotent dan mengganti seluruh representasi; PATCH memodifikasi sebagian. Kalau kamu hanya mengirim field yang berubah, gunakan PATCH.

Hierarki dan Hubungan Resource

Hubungan antar resource dimodelkan lewat URL bersarang, tapi jangan berlebihan. Maksimal dua level umumnya cukup:

GET /api/customers/42/orders

Lebih dalam dari itu jadi sulit dipahami dan memaksa konsumen mengingat struktur yang rumit. Untuk hubungan yang jarang diakses, gunakan query parameter atau resource flat:

GET /api/orders?customerId=42

Aturan praktis: sarang jika resource child tidak bisa ada tanpa parent. Jika child bisa diakses independen, buat flat.

HTTP Status Code: Jangan Selalu 200

Salah satu kesalahan paling umum adalah mengembalikan 200 OK untuk semua kondisi, lalu menaruh status sebenarnya di body JSON. Ini menghancurkan kemampuan client, cache, dan monitoring untuk memahami apa yang terjadi.

Gunakan kode status dengan benar:

  • 200 — sukses baca/update
  • 201 — resource berhasil dibuat (sertakan header Location)
  • 204 — sukses tanpa body (misal DELETE)
  • 400 — request tidak valid
  • 401 — belum terautentikasi
  • 403 — sudah login tapi tidak punya akses
  • 404 — resource tidak ditemukan
  • 409 — konflik state (misal resource sudah ada)
  • 422 — validasi gagal
  • 429 — rate limit terlampaui
  • 500 — error internal server

Perbedaan 401 vs 403 penting: 401 berarti "kamu belum login (atau token expired)", 403 berarti "kamu sudah login tapi dilarang akses resource ini".

Format Error yang Konsisten

Konsumen API butuh format error yang stabil untuk menangani kegagalan secara terprogram. Jangan campur aduk: kadang string, kadang object.

Contoh format yang baik:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Field 'email' tidak valid",
    "details": [
      {
        "field": "email",
        "reason": "format_email_tidak_valid"
      }
    ],
    "requestId": "req_8f3a2c"
  }
}

Selalu sertakan requestId untuk korelasi log. Konsumen yang bisa menyerahkan requestId saat melapor bug akan sangat memudahkan debugging.

Versioning: URL vs Header

Versioning adalah keputusan yang mahal untuk diubah belakangan. Dua pendekatan umum:

  1. URL prefix/api/v1/users, /api/v2/users. Eksplisit, mudah di-cache, mudah dites, tapi dianggap "kurang RESTful" oleh purist karena resource yang sama punya banyak URL.
  2. HeaderAccept: application/vnd.company.v2+json. Lebih "benar" secara teoretis, tapi menyulitkan caching di CDN dan pengujian manual.

Untuk kebanyakan tim, URL versioning lebih pragmatis. Ia sederhana, terlihat jelas di log, dan tidak butuh negosiasi konten yang rumit. Version hanya saat ada breaking change, bukan setiap rilis kecil.

Pagination, Filtering, dan Sorting

Endpoint yang mengembalikan koleksi wajib punya pagination. Tanpa itu, dataset besar akan membunuh performa.

Gunakan query parameter yang konsisten:

GET /api/orders?page=2&perPage=50&sort=createdAt&order=desc&status=PAID

Selalu kembalikan metadata pagination:

{
  "data": [...],
  "meta": {
    "page": 2,
    "perPage": 50,
    "total": 1043,
    "totalPages": 21,
    "hasNext": true
  }
}

Untuk dataset yang sangat besar dan sering berubah, pertimbangkan cursor-based pagination (before/after token) alih-alih offset, karena offset bisa melewatkan atau menduplikasi data saat data bergeser di tengah pagination.

Autentikasi dan Otorisasi

Autentikasi (siapa kamu) dan otorisasi (apa yang boleh kamu lakukan) adalah dua lapis yang berbeda. Jangan pernah menggabungkan keduanya menjadi satu check.

  • Autentikasi: gunakan token bearer (JWT atau opaque token) via header Authorization: Bearer <token>. Jangan pernah meletakkan token di URL — URL masuk ke log dan browser history.
  • Otorisasi: enforce di server untuk setiap endpoint, bukan hanya menyembunyikan menu di client. Client-side hiding bukan security control.

JWT populer karena stateless, tapi punya trade-off: token tidak bisa di-revoke dengan mudah sampai expiry. Untuk sistem yang butuh revoke langsung, gunakan opaque token dengan lookup database atau short-lived access token + refresh token.

Rate Limiting dan Throttling

API publik wajib punya rate limiting. Ini melindungi dari abuse dan menjaga stabilitas.

Sertakan header standar agar konsumen bisa beradaptasi:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 43
X-RateLimit-Reset: 1691234567

Ketika limit terlampaui, kembalikan 429 Too Many Requests dengan header Retry-After.

Rate limiting bisa diimplementasi per-IP, per-API-key, atau per-user. Untuk API berbayar, per-API-key adalah standar.

Idempotency untuk Operasi Tidak Aman

POST yang membuat resource (misal pembayaran) bisa gagal di tengah jalan — response hilang, tapi server sudah memproses. Konsumen yang retry bisa membuat resource duplikat.

Solusinya: idempotency key. Konsumen mengirim header unik, server menyimpan hasil pertama dan mengembalikan hasil yang sama untuk key yang sama:

Idempotency-Key: 8f3a2c-...

Ini krusial untuk API pembayaran, transfer, dan operasi yang tidak boleh terduplikasi. Tanpa ini, retry otomatis bisa menghasilkan transaksi ganda.

Kapan REST Bukan Jawaban

REST bukan solusi universal. Beberapa kasus di mana alternatif lebih baik:

  • Kebutuhan fleksibilitas query tinggi di client → GraphQL. Client memilih field yang dibutuhkan, mengurangi over-fetching. Trade-off: kompleksitas server dan masalah N+1.
  • Komunikasi internal service-to-service dengan latensi rendah → gRPC. Binary protocol, HTTP/2, codegen otomatis. Trade-off: tidak mudah di-debug dengan browser.
  • Real-time → WebSocket atau SSE. REST tidak dirancang untuk push berkelanjutan.

Jangan memakai GraphQL hanya karena tren. Kalau konsumen kamu adalah tim frontend yang butuh data tetap, REST yang terdesain baik seringkali sudah cukup dan lebih sederhana untuk dioperasikan.

Kesalahan Umum yang Harus Dihindari

  1. Menaruh kata kerja di URLGET /api/getUsers salah; GET /api/users benar.
  2. Mengembalikan 200 untuk error — hancurkan monitoring dan penanganan client.
  3. Mengekspos struktur database langsung — resource tidak sama dengan tabel. Sembunyikan implementasi internal.
  4. Tidak ada pagination di list endpoint — dataset tumbuh, API mati.
  5. Token di URL — bocor ke log dan history.
  6. Melupakan idempotency di operasi keuangan — retry menghasilkan transaksi ganda.
  7. Versioning tanpa strategi — perubahan kecil memaksa semua konsumen migrasi mendadak.

FAQ

Apakah saya harus pakai HATEOAS? HATEOAS (Hypermedia as the Engine of Application State) adalah batasan REST yang paling jarang diimplementasi di dunia nyata. Sebagian besar API publik memilih kontrak statis via dokumentasi alih-alih hypermedia dinamis. Kecuali kamu membangun sistem dengan konsumen yang benar-benar dinamis, ini seringkali over-engineering.

JSON atau format lain? JSON adalah default de-facto karena dibaca manusia dan didukung semua bahasa. Untuk payload besar atau efisiensi tinggi, pertimbangkan protobuf/msgpack, tapi jangan tanpa alasan kuat.

Berapa lama saya harus support versi lama? Tergantung kontrak dengan konsumen. Umumnya deprecate versi lama dengan pemberitahuan minimal 6 bulan, pantau usage sebelum mematikan, dan jangan pernah mematikan versi yang masih dipakai tanpa komunikasi.

OpenAPI/Swagger penting? Ya, sangat. Spesifikasi OpenAPI memungkinkan dokumentasi otomatis, codegen client, dan validasi kontrak. Jadikan OpenAPI sebagai source of truth, bukan dokumen yang ditulis belakangan.

Kesimpulan Praktis

REST API yang baik adalah kontrak yang dirancang, bukan kumpulan endpoint yang kebetulan ada. Mulai dari resource modeling yang benar, hormati semantik HTTP, gunakan status code dan error format yang konsisten, dan siapkan versioning serta idempotency sejak awal.

Empat keputusan yang paling menentukan kualitas jangka panjang: resource modeling yang tepat, status code yang jujur, pagination yang selalu ada, dan idempotency untuk operasi tidak aman. Sisanya bisa disesuaikan seiring pertumbuhan, tapi keempat hal ini mahal untuk diperbaiki belakangan.

API adalah janji kepada konsumen. Sekali dirilis, ia hidup lebih lama dari kebanyakan kode yang menghasilkannya. Rancang dengan rasa hormat terhadap umur panjang itu.