Skip to content
jsonforge.app
Kembali ke blog
Panduan9 menit baca

Desain Respons JSON REST API: Praktik Terbaik dan Anti-Pattern

Byte yang dikembalikan REST API sama pentingnya dengan route dan kode status. Setiap klien yang memanggil API Anda ditulis berdasarkan bentuk JSON-nya, dan mengubah bentuk JSON itu nanti akan merusak klien, sekecil apa pun perubahannya terasa di internal. Panduan ini membahas keputusan desain respons yang murah jika benar sejak awal dan mahal diperbaiki setelah klien ada.

Pilih satu bentuk envelope dan jangan pernah menyimpang

Envelope adalah pembungkus level atas yang konsisten untuk setiap respons, apa pun endpoint-nya: sekumpulan key yang dikenal seperti `data`, `error`, dan `meta` yang bisa ditangani secara generik oleh lapisan HTTP klien alih-alih menulis parsing khusus per endpoint. Tanpanya, setiap endpoint yang mengembalikan bentuk sedikit berbeda memaksa setiap klien menulis parser yang sedikit berbeda.

Envelope yang konsisten: respons sukses dan error berbagi bentuk level atas yang sama.
json
// Success
{
  "data": { "id": 42, "name": "Ada Lovelace" },
  "meta": { "requestId": "a1b2c3" }
}

// Failure
{
  "data": null,
  "error": { "code": "NOT_FOUND", "message": "User 42 does not exist" },
  "meta": { "requestId": "a1b2c4" }
}

Nama key yang dipilih kurang penting dibanding konsistensinya: apa pun pilihan Anda, setiap endpoint mengembalikannya, `data` selalu menjadi tempat payload berada, dan klien bisa menulis satu fungsi `unwrap(response)` untuk seluruh API alih-alih satu per resource.

Jangan pernah mengembalikan array (atau skalar) telanjang di level atas

`GET /users` yang mengembalikan array JSON telanjang (`[{...}, {...}]`) tampak wajar, tetapi Anda tidak bisa menambahkan metadata level respons nanti tanpa breaking change. Begitu Anda butuh total count, cursor halaman berikutnya, atau pesan peringatan di samping daftar, Anda harus mengubah tipe level atas dari array ke object, dan itu merusak setiap klien yang memanggil `response.map(...)` atau `response.length` pada body hasil parsing.

Bungkus koleksi dalam field bernama sejak hari pertama (`{ "users": [...] }` atau `{ "data": [...] }`), meskipun Anda yakin tak akan butuh metadata tambahan. Menambahkan key saudara pada object tetap backward-compatible. Mengubah array menjadi object merusak klien mana pun dalam bahasa apa pun.

Null vs. dihilangkan: keduanya bukan sinyal yang sama

Field yang hadir dengan nilai `null` dan field yang ditinggalkan keluar dari respons sama-sama tampak seperti 'tanpa nilai' sekilas, tetapi keduanya berarti hal berbeda dan klien perlu bisa membedakannya. `null` berkata: field ini konsep nyata yang dikenal untuk resource ini, dan nilai saat ini secara eksplisit kosong (pengguna tanpa `middleName`). Dihilangkan berkata: field ini tidak berlaku, tidak dimuat, atau tidak diminta (permintaan sparse fieldset yang hanya meminta `id` dan `name`).

Semantik PATCH bergantung pada perbedaan ini. Dalam JSON Merge Patch (RFC 7396), mengirim field sebagai `null` menghapus field itu dari target, sedangkan tidak menyertakan field dalam body patch membiarkannya tetap. Jika Anda mencampuradukkan keduanya, atau tidak jelas soal field mana yang boleh `null`, klien tidak bisa membedakan 'kosongkan nilai ini' dari 'saya tidak mengirim pembaruan untuk ini'.

Pagination: cursor vs. offset, dan bagaimana bentuk JSON-nya

Pagination offset/limit (`?offset=40&limit=20`, atau `?page=3&pageSize=20`) paling sederhana untuk diimplementasikan dan dipahami, tetapi punya masalah kebenaran yang nyata: jika baris disisipkan atau dihapus di antara permintaan halaman, offset bergeser di bawah kaki klien, menyebabkan baris terlewat atau terduplikasi. Ia juga mahal di SQL pada offset dalam, karena database tetap harus memindai dan membuang semua baris yang dilewati.

Pagination berbasis cursor memberi klien token opaque yang menunjuk 'baris setelah yang terakhir Anda lihat', bukan posisi angka, sehingga hasilnya tetap stabil saat data berubah dan query tetap murah di kedalaman berapa pun. Trade-off-nya, klien tidak bisa melompat ke nomor halaman sembarang. Klien hanya bisa maju (dan, jika didukung, mundur) dari sebuah cursor.

Respons pagination berbasis cursor: tanpa hitungan offset, hanya token halaman berikutnya yang opaque.
json
{
  "data": [ { "id": 101 }, { "id": 102 } ],
  "meta": {
    "nextCursor": "eyJpZCI6MTAyfQ==",
    "hasMore": true
  }
}

Jadikan pagination berbasis cursor sebagai bawaan untuk data yang sering berubah (feed, log, event stream) atau tabel besar. Pagination berbasis offset cocok untuk koleksi kecil yang cenderung statis, di mana lompat ke halaman N memang membantu (tabel admin dengan pemilih halaman).

Object error dan konvensi penamaan

Respons error seharusnya membawa lebih dari kode status HTTP dan sebuah string: `code` yang stabil dan dapat dibaca mesin yang bisa aman di-branch oleh klien (`"VALIDATION_ERROR"`, bukan kalimat), `message` yang dapat dibaca manusia untuk log dan pengembang alih-alih untuk diparse, dan untuk kegagalan validasi, array `details` yang menyebutkan persis field mana yang gagal dan mengapa.

Bentuk error yang bisa ditindaklanjuti klien.
json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request failed validation",
    "details": [
      { "field": "email", "message": "must be a valid email address" },
      { "field": "age", "message": "must be at least 0" }
    ]
  }
}

Untuk penamaan key, camelCase dan snake_case sama-sama bisa dipakai. Masalah muncul saat Anda mencampur keduanya antar-endpoint dalam satu API, karena setiap klien lalu butuh logika pemetaan field yang bergantung pada endpoint. Konsumen JavaScript/TypeScript cenderung memilih camelCase karena cocok dengan destructuring native; ekosistem Python dan Ruby sering memilih snake_case. Pilih satu untuk seluruh API, dan terapkan juga pada boolean (`isActive`/`hasAccess`, bukan campuran `active` dan `has_access`).

Anti-pattern yang umum

JSON yang di-stringify di dalam field JSON (`"metadata": "{\"plan\":\"pro\"}"` alih-alih `"metadata": { "plan": "pro" }`) memaksa setiap klien mem-parse dua kali dan membuang type safety tanpa manfaat. Biasanya ini tanda bahwa server menyerialisasi kolom blob database apa adanya tanpa mendekodenya sebelum mengirim respons.

Format tanggal yang tidak konsisten (sebagian field berupa timestamp Unix, sebagian `"08/15/2026"`, sebagian lagi ISO 8601 penuh) memaksa klien menulis parsing tanggal per field, bukan satu handler tanggal bersama. Standarkan pada ISO 8601 dalam UTC dengan offset eksplisit (`"2026-08-15T09:30:00Z"`) di semua tempat.

Membocorkan field database internal berarti mengembalikan artefak ORM seperti `password_hash`, foreign key internal milik layanan lain, atau field pembukuan (`__v`, `_id`, `created_by_worker_id`) apa adanya dalam respons API. Petakan model database Anda ke response DTO yang eksplisit, jangan menyerialisasi modelnya langsung. DTO adalah cara andal untuk mencegah perubahan schema internal berubah menjadi perubahan API publik.

FAQ

Apakah respons error sebaiknya memakai envelope yang sama dengan respons sukses?
Ya. Jika respons sukses berbentuk `{ "data": ..., "meta": ... }`, respons error sebaiknya memakai bentuk level atas yang sama dengan `data: null` dan object `error` terisi. Dengan begitu kode penanganan respons di klien cukup memeriksa satu hal, yaitu ada tidaknya `error`, tanpa bercabang berdasarkan bentuk respons per kode status.
camelCase atau snake_case: apakah pilihannya berpengaruh?
Konvensinya sendiri tidak terlalu berpengaruh; konsistensinya yang berpengaruh besar. Pilih satu konvensi untuk seluruh API berdasarkan ekosistem konsumen utama Anda, dokumentasikan, dan terapkan di mana-mana, termasuk prefix boolean dan key object bersarang. Biaya muncul dari API yang endpoint-nya memakai konvensi berbeda-beda sehingga setiap klien butuh logika pemetaan khusus per endpoint.
Apakah API baru sebaiknya memakai pagination cursor atau offset?
Default-kan pagination berbasis cursor kecuali Anda secara khusus perlu mengizinkan pengguna melompat ke nomor halaman arbitrer pada data kecil yang sebagian besar statis. Pagination cursor tetap benar saat baris ditambah atau dihapus di tengah pagination dan tetap murah di kedalaman apa pun; pagination offset memburuk dalam kebenaran maupun performa seiring dataset tumbuh atau berubah.
Mengapa tidak langsung menyerialisasi model database sebagai respons API saja?
Schema database dan kontrak API publik Anda berubah karena alasan berbeda dan pada waktu berbeda. Jika Anda menyerialisasi model secara langsung, setiap migrasi, kolom baru, atau upgrade ORM bisa mengubah atau membocorkan bentuk respons publik tanpa disadari. Response DTO yang eksplisit memisahkan keduanya, dengan biaya satu langkah pemetaan tambahan per endpoint.

Coba alat ini

Artikel terkait