JSON Toolkit

Poin Penting dalam Mendesain Kontrak Data JSON API

Intinya: Enam hal sangat sulit diubah setelah rilis: envelope respons, format error, bentuk pagination, representasi ID dan waktu, semantik null versus tidak ada, serta evolusi versi.

Menengah±13 menit bacaDiterbitkan 2026-10-30Diperbarui 2026-10-30
Sampul poin penting mendesain kontrak data JSON API

Intinya: kunci envelope respons, format error, representasi ID dan waktu, serta semantik null-versus-tidak-ada di dalam kontrak sebelum berdebat soal detail pagination dan penamaan; selalu bungkus top level dengan envelope data agar pagination dan tracing ID bisa ditambahkan nanti tanpa memutus pemanggil yang sudah ada.

Tidak ada jawaban standar dalam desain API, tetapi ada keputusan "sulit ditarik kembali setelah dipilih" yang sebaiknya dipikirkan matang sebelum Anda menulis baris kode pertama.

1. Membungkus respons atau tidak

Tiga gaya:

// Gaya A: respons polos
{ "id": 1, "name": "张三" }

// Gaya B: envelope data
{ "data": { "id": 1, "name": "张三" } }

// Gaya C: data + metadata
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
Gaya Kelebihan Kekurangan
Respons polos Paling sederhana, satu lapisan lebih sedikit untuk frontend Tidak bisa menempelkan metadata; array di top level membawa risiko JSON hijacking dan evolusi
Envelope data Mudah menambahkan meta / errors / links Satu level nesting tambahan, frontend harus menulis res.data.data
Data + metadata Observabilitas terbaik Butuh kesepakatan ketat soal field mana yang masuk meta

Rekomendasi: gunakan envelope data secara seragam untuk daftar maupun detail, dan jangan pernah menjadikan top level sebagai array. Dengan begitu, menambahkan pagination atau tracing ID nanti tidak pernah memutus kontrak.

2. Format error: tiru RFC 9457

Menciptakan struktur error sendiri adalah sumber biaya kolaborasi yang besar. RFC 9457 (yang menggantikan RFC 7807) mendefinisikan application/problem+json:

{
  "type": "https://bbdrug.com/problems/insufficient-balance",
  "title": "余额不足",
  "status": 422,
  "detail": "账户余额 12.00 元,本次扣款需要 30.00 元",
  "instance": "/accounts/A1001/payments/p-889",
  "balance": "12.00",
  "required": "30.00"
}
Field Arti Wajib
type URI stabil untuk tipe masalah Disarankan
title Ringkasan singkat yang mudah dibaca manusia Disarankan
status Kode status HTTP Disarankan
detail Penjelasan spesifik tentang apa yang terjadi kali ini Opsional
instance Instance resource yang bermasalah Opsional
Field ekstensi Field bisnis sembarang Opsional

Field ekstensi (seperti balance / required di atas) diizinkan spesifikasi, dan frontend bisa memakainya untuk pesan yang presisi alih-alih mem-parsing teks detail.

3. Pagination: cursor mengalahkan offset

// Pagination offset
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }

// Pagination cursor
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
Dimensi Pagination offset Pagination cursor
Kesulitan implementasi Rendah (LIMIT/OFFSET) Menengah (butuh key urutan yang stabil)
Performa pada halaman dalam Buruk (semakin besar offset, semakin lambat) Baik
Ketika data berubah Duplikat atau item hilang Stabil
Lompat ke halaman Bisa Tidak bisa
Butuh total count Butuh count tambahan Tidak perlu

Rekomendasi: gunakan pagination offset untuk tabel admin (mereka butuh lompat halaman dan total), dan pagination cursor untuk feed tak berujung yang menghadap pengguna.

4. ID dan waktu

4.1 Selalu gunakan string untuk ID

Alasannya sudah dibahas di artikel tentang tipe: di sisi JavaScript, nilai di atas 2^53 diam-diam kehilangan presisi. Bahkan jika ID saat ini adalah integer auto-increment kecil, memakai string menyisakan ruang untuk beralih ke snowflake ID nanti.

{ "id": "1000000000000000001" }

4.2 Gunakan string RFC 3339 untuk waktu

{
  "createdAt": "2026-10-30T10:41:45+08:00",
  "updatedAt": "2026-10-30T02:41:45Z"
}
  • Jangan gunakan number timestamp milidetik: sulit dibaca, dan satuannya (detik/milidetik) sering keliru;
  • Tentukan zona waktu secara eksplisit: dengan Z atau dengan offset; jangan pernah memakai "waktu lokal tanpa zona waktu";
  • Beri nama field secara seragam xxxAt agar sekilas jelas bahwa itu sebuah titik waktu.

5. null dan tidak ada

Anda harus memilih satu dan menuliskannya ke dokumentasi:

Pendekatan Menyatakan "tidak ada nilai" Menyatakan "jangan ubah"
Pendekatan A Nilai field adalah null Hilangkan field di request
Pendekatan B Field tidak ada Hilangkan field di request

Masalah Pendekatan A adalah "dihilangkan" dan "dikirim sebagai null" punya arti berbeda dalam PATCH, yang mudah disalahpahami; masalah Pendekatan B adalah JSON.stringify di frontend membuang field yang nilainya undefined, yang kebetulan cocok dengan semantiknya.

Rekomendasi praktis:

  • Gunakan Pendekatan A untuk response body (null jelas berarti tidak ada nilai, dan frontend tidak perlu membedakan undefined);
  • Gunakan Pendekatan B untuk request body (terutama PATCH), dikombinasikan dengan Merge Patch atau JSON Patch agar semantiknya eksplisit.

6. Penamaan dan struktur field

Keputusan Pilihan umum Catatan
Gaya penamaan camelCase (ekosistem JS) / snake_case (Python, basis data) Seragam di seluruh proyek, jangan dicampur
Nilai enum Huruf kecil dengan garis bawah, "pending_review" Jangan pakai number; sulit dibaca
Field boolean isActive / enabled Jangan pakai status: 0/1
Field array Kata benda jamak, items Kembalikan [] untuk array kosong, jangan pernah null
Kedalaman nesting Tidak lebih dari 3 level Lebih dalam dari itu, pecah atau tawarkan parameter expand

Mengembalikan [] alih-alih null untuk array kosong sangat penting: data.items.map(...) di frontend tidak akan crash karena sebuah null.

7. Strategi evolusi

Perubahan Memutus kompatibilitas Cara menangani
Menambah field opsional Tidak Kirim langsung
Menambah field wajib Ya Beri nilai default, atau rilis lewat versi
Menghapus field Ya Tandai deprecated dulu, amati pemanggil, lalu hapus
Mengubah tipe field Ya Tambah nama field baru dan dual-write selama transisi
Mengubah himpunan nilai enum Bergantung Klien harus punya fallback untuk nilai tak dikenal
Mengubah semantik field Ya Perlakukan sebagai field baru

Tiga aturan disiplin:

  1. Klien harus toleran terhadap field tak dikenal (jangan mengurung diri dengan additionalProperties: false);
  2. Klien harus punya cabang fallback untuk nilai enum tak dikenal;
  3. Pertahankan field deprecated setidaknya satu versi mayor, dan tandai dengan Deprecation di header respons atau dokumentasi.

8. Contoh lengkap

{
  "data": [
    {
      "id": "1001",
      "name": "JSON 权威指南",
      "price": "89.50",
      "tags": ["json", "spec"],
      "publishedAt": "2026-09-01T00:00:00+08:00",
      "author": null
    }
  ],
  "page": { "nextCursor": null, "hasMore": false },
  "meta": { "requestId": "req-7f3a", "tookMs": 12 }
}

Respons ini mencerminkan semua konvensi di atas: envelope data, ID berupa string, jumlah uang sebagai string, timestamp dengan zona waktu, null bermakna tidak ada, array kosong, pagination cursor, dan meta untuk informasi observabilitas.

9. Checklist

  • [ ] Top level bukan array; gunakan envelope data secara seragam;
  • [ ] Respons error mengikuti RFC 9457 dan menyertakan type yang stabil;
  • [ ] Bentuk pagination sesuai skenario bisnis;
  • [ ] ID berupa string, dan waktu memakai RFC 3339;
  • [ ] Semantik null dan tidak ada dituliskan di dokumentasi;
  • [ ] Gaya penamaan seragam di seluruh proyek, dan enum memakai string huruf kecil;
  • [ ] Klien punya fallback untuk field tak dikenal dan nilai enum tak dikenal;
  • [ ] Perubahan yang memutus kompatibilitas melewati versioning atau transisi dual-write.

FAQ

Bagaimana cara mendesain JSON API yang bisa berevolusi?

Bungkus daftar dan detail secara seragam dalam envelope data dan jangan pernah menjadikan top level sebagai array, agar pagination dan tracing ID bisa ditambahkan tanpa memutus kontrak. Gunakan ID string dan timestamp RFC 3339, dan tuliskan semantik null-versus-tidak-ada di dokumentasi. Jaga field baru tetap opsional dan mandiri secara semantik, dan beri klien fallback untuk field serta nilai enum tak dikenal.

Mengapa tidak menciptakan format error kustom?

Menciptakan struktur error sendiri adalah sumber biaya kolaborasi yang besar. Tiru application/problem+json RFC 9457, identifikasi masalah dengan type yang stabil, dan izinkan field ekstensi seperti balance dan required, agar frontend bisa membangun pesan presisi tanpa mem-parsing teks detail.

Apakah setiap endpoint harus memakai pagination cursor?

Tidak. Tabel admin butuh lompat halaman dan total, sehingga pagination offset lebih cocok; feed tak berujung yang menghadap pengguna sebaiknya memakai pagination cursor, yang tetap stabil di halaman dalam dan menghindari duplikat atau celah ketika data berubah. Pilih berdasarkan apakah skenario itu butuh lompat halaman dan total.