跳至內容

REST API 的 JSON 最佳實務

命名、日期、ID、錯誤封包、分頁、版本控制——那些經得起時間考驗的 JSON 設計決策,與不行的那些。

REST API 在 JSON 上積累了數十年的慣例。遵循慣例可以無痛接上各種 客戶端函式庫、代碼生成器與開發者既有的心智模型;偏離慣例會帶來 永無止盡的支援工單。本文是有立場的劇本:經得起時間的選擇,以及 每項決定背後的理由。

一致的命名

選一種命名風格、貫徹到底。兩個合理選項:

  • camelCase——JavaScript 原生,給 web 與行動端使用最自然,是新 API 的主流。
  • snake_case——Python、Ruby 原生,常見於老牌 API(Stripe、GitHub)與資料工程脈絡。

kebab-casePascalCase 偶爾出現,但會在用 . 存取成員的語言 造成摩擦,避免。

規則是 一致,不是哪一種。混用——userId 旁邊放 created_at—— 是最糟的選擇;如果內部使用另一種,就在邊界轉換。

回應形狀

「列出資源」endpoint 三種合理形狀:

裸陣列——GET /users 回傳 [{...}, {...}]。最簡單。無法加 meta (分頁、總數)而不破壞客戶端。只在永不分頁的 endpoint 使用。

封套物件——GET /users 回傳 { "data": [...], "meta": {...} }。 日後可加 meta 而不破壞舊客戶端。新 API 的預設選擇。

{
  "data": [
    { "id": "user_1", "name": "Ada" },
    { "id": "user_2", "name": "Alan" }
  ],
  "meta": {
    "page": 1,
    "pageSize": 20,
    "total": 142
  }
}

JSON:API——{"data": [...], "included": [...], "links": {...}}。 重量級但有規範,免費處理關聯。當你的領域真的是圖狀且消費端能從共用 規格受益時最佳。

不要自創新的包裝——從這三者擇一。

錯誤:RFC 7807

錯誤請用 RFC 7807 Problem Details, 它是標準錯誤格式,Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/insufficient-funds",
  "title": "Insufficient funds",
  "status": 402,
  "detail": "Your account balance is $5; the transaction requires $10.",
  "instance": "/accounts/acct_123/transactions/txn_456",
  "balance": 5,
  "required": 10
}

五個欄位:

  • type——識別錯誤類別的 URI(客戶端依此分派)。
  • title——人類可讀的簡短摘要。
  • status——HTTP 狀態碼(與回應一致)。
  • detail——人類可讀的詳細說明。
  • instance——特定發生點的 URI(對客服很有用)。

加上特定錯誤類別的 擴充欄位。驗證錯誤通常會帶 errors 陣列, 列出欄位路徑與訊息。

型別:數字、日期、金額、ID

四個最常踩雷的型別決定:

數字。JSON 數字是十進位文字,但 JavaScript 客戶端解析成 64-bit float。超過 ±2^53 就會失真。三類值必須以字串編碼:

  • ID——"user_123""123456789012345"——字串。即使內部是數字 ID,輸出也應字串化以避免失真並保留未來彈性。
  • 金額——含小數位的字串("19.95"),或最小幣別單位的整數(1995 表示 $19.95)。絕不要用浮點。
  • 大型計數——字串。

日期。ISO 8601 字串,含時區。永遠。

{
  "createdAt": "2026-05-13T10:30:00Z",
  "birthday": "1985-03-12"
}

完整時間戳記以 UTC(Z 後綴);純日期不含時間。不要用秒數的 Unix 時間戳——它在精度上模糊、人類不可讀、客戶端必須知道哪些欄位 是日期。

布林。只有 truefalse。不要用 "yes"/"no"0/1

Enum。字串、全小寫、kebab-case 或 snake-case 與全 API 一致。 "status": "in-progress",不要 "status": 2

null、缺值、空字串

三種狀態有不同意義:

  • 缺值(沒有 "middleName" 鍵)——「我們沒這資料,或不適用」。
  • null"middleName": null)——「我們問過,使用者沒填」。
  • 空字串"middleName": "")——通常是 UI bug;使用者在必填欄沒輸入。在 API 端轉成 null 或直接拒絕。

請在「缺值」與「null」之間擇一作為「不存在」並保持一致。混用(有時 鍵不在、有時為 null)會導致無止盡的客戶端 bug。多數現代 API 永遠 輸出鍵、以 null 表示不存在——對型別生成器與 TypeScript 都更友善。

分頁

兩種模式:

  • 頁碼——?page=2&pageSize=20。容易理解、能曝露總數,但在資料新增/刪除時分頁會跳。
  • Cursor——?cursor=opaque-token。在插入與刪除下穩定;任何 append-heavy 資源(事件、日誌、訊息)的正確選擇。

不論哪種,請在回應中回傳 next cursor 或 next 連結,讓客戶端不必 自己算:

{
  "data": [...],
  "links": {
    "next": "https://api.example.com/events?cursor=eyJ0IjoxNzMxNTAwMDAwfQ"
  }
}

版本

影響最長期的決定,三種真實選項:

  • URL——/v1/users/v2/users。最顯眼、最容易路由、鼓勵大型遷移。
  • Header——Accept: application/vnd.example.v2+json。URL 乾淨,但版本對使用者隱形。
  • 連續式——不分版本,只能加新欄位與新 endpoint,不可移除或改名。當你能控制兩端時最佳。

連續式做得到就值得。否則 URL 版本最少意外。

比格式更重要的規則:加欄位不算 breaking change。客戶端應忽略未知 欄位。移除或更名是 breaking,需升版。

傳輸:Content-Type、壓縮、最小化

  • Content-Type——回應用 application/json,錯誤用 application/problem+json。2026 年已無需指定 charset——UTF-8 是規格預設。
  • 壓縮——gzip 或 brotli。超過幾百位元組必開。
  • 最小化——回應 yes、版控 fixture 與設定 no。請見 壓縮與美化 JSON
  • Content-LengthTransfer-Encoding: chunked——擇一設定。大型回應改用串流。

以 JSON Schema 與 OpenAPI 文件化

可給工具吃的 JSON 合約勝過散文文件十倍。兩條可行路徑:

  • 每個 endpoint 一份 JSON Schema——為每個請求/回應寫一份。同時用於執行期驗證(伺服端)與型別生成(客戶端)。見 JSON Schema 新手入門
  • OpenAPI——把 schema、URL、auth、範例打包成一份文件。每個 API client 生成器(openapi-typescriptopenapi-generator-cli)都吃。

內部服務雙端皆你掌控時,schema 驅動與 OpenAPI 都行。公開 API 請選 OpenAPI,那是生態圈預期的格式。

工具

延伸閱讀