結論先講

API 是前後端的合約。合約寫得好,大家照著走就不太會出事;寫得爛,每次串接都是一場災難。

好的 API 有三個核心原則:可預測(看到 URL 就知道做什麼)、一致(錯誤格式、分頁方式全站統一)、好除錯(回傳足夠的資訊讓人知道哪裡出錯)。

這篇整理了 12 個 API 設計的檢查點,不管你做 REST、GraphQL 還是 gRPC,概念都通用(但範例以 REST 為主)。


體檢清單

1. RESTful 規範

REST 不是宗教,不用死守每條教義,但基本原則要遵守:

# 好的
GET    /api/users          # 取得列表
GET    /api/users/123      # 取得單筆
POST   /api/users          # 建立
PUT    /api/users/123      # 完整更新
PATCH  /api/users/123      # 部分更新
DELETE /api/users/123      # 刪除

# 不好的
GET    /api/getUsers
POST   /api/createUser
POST   /api/deleteUser/123
GET    /api/user/update?id=123&name=foo
  • 用名詞不用動詞(資源導向)
  • 複數形式(/users 不是 /user
  • 巢狀資源有限制深度(最多兩層)
  • 動作用 HTTP method 表達

2. 統一錯誤格式

全站所有 API 的錯誤回應長一樣。 這是最容易被忽略但影響最大的設計。

{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "User with ID 123 not found",
    "details": [],
    "requestId": "req_abc123"
  }
}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request body",
    "details": [
      { "field": "email", "message": "must be a valid email" },
      { "field": "age", "message": "must be >= 0" }
    ],
    "requestId": "req_def456"
  }
}
  • 錯誤有 machine-readable code(不是只有 message)
  • 驗證錯誤列出每個欄位的問題
  • 每個 request 有唯一 ID(方便 debug)

3. 分頁策略

方式優點缺點適用場景
Offset簡單、可跳頁資料變動時會重複/遺漏後台管理、資料穩定
Cursor效能好、不會遺漏不能跳頁動態 feed、大量資料
Keyset效能最好實作稍複雜高效能需求
// Cursor-based pagination 回應
{
  "data": [...],
  "pagination": {
    "hasNext": true,
    "hasPrev": false,
    "nextCursor": "eyJpZCI6MTAwfQ==",
    "totalCount": 1523
  }
}
  • 有分頁(不要一次回傳全部資料)
  • 回傳 totalCount(或至少 hasNext
  • 預設 page size 合理(20-50)
  • 有 max page size 限制

4. Filtering / Sorting

GET /api/products?category=electronics&price_min=100&price_max=500
GET /api/products?sort=-created_at,name
GET /api/users?search=terry&role=admin
  • 篩選用 query parameter
  • 排序用 sort 參數(- 前綴代表降序)
  • 全文搜尋用 searchq 參數

5. Versioning 策略

方式範例優點缺點
URL Path/api/v1/users最直觀URL 會變
HeaderAccept: application/vnd.api+json;version=1URL 不變不好測試
Query/api/users?version=1簡單不夠正式
  • 有版本策略(推薦 URL path)
  • 舊版本有 deprecation 通知
  • breaking change 才升版號

6. Idempotency(冪等性)

同一個請求送兩次,結果應該一樣。

# 冪等的
GET    /users/123      → 一定拿到同一個 user
PUT    /users/123      → 結果一樣
DELETE /users/123      → 第二次回 404,但沒有副作用

# 不冪等的
POST   /users          → 會建立兩個 user(除非你處理了)
  • PUT、DELETE 是冪等的
  • POST 需要防重複(Idempotency-Key header)
  • 支付相關 API 一定要有冪等機制
# Stripe 風格的 Idempotency Key
POST /api/payments
Idempotency-Key: key_unique_123

7. Rate Limiting Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 67
X-RateLimit-Reset: 1710500000
Retry-After: 30
  • 回傳 rate limit 資訊
  • 429 回應包含 Retry-After
  • 不同 API 可以有不同限制

8. CORS

  • 明確設定允許的 origin(不要用 *
  • 只允許必要的 HTTP methods
  • Preflight 回應有 cache(Access-Control-Max-Age

9. Authentication

  • 使用 Bearer Token(Authorization: Bearer <token>
  • Token 過期時間合理
  • 401 和 403 分清楚(未認證 vs 無權限)

10. API Documentation

# OpenAPI 3.0 範例
paths:
  /users:
    get:
      summary: List all users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'
  • 有 OpenAPI / Swagger 文件
  • 每個 endpoint 有範例
  • 文件跟程式碼同步(自動產生最好)

11. Response Envelope

// 統一的回應格式
{
  "data": { ... },
  "meta": {
    "requestId": "req_abc",
    "timestamp": "2026-03-15T10:00:00Z"
  }
}
 
// 列表回應
{
  "data": [...],
  "pagination": { ... },
  "meta": { ... }
}
  • 統一的回應結構
  • 列表和單筆有區分
  • meta 資訊(request ID、時間戳)

12. HTTP Status Codes

狀態碼何時用
200成功
201建立成功
204成功但無內容(DELETE)
400請求格式錯誤
401未認證
403無權限
404資源不存在
409衝突(重複建立等)
422驗證失敗
429太多請求
500伺服器錯誤
  • 正確使用 status code(不要全部回 200)
  • 不要自創 status code
  • 4xx 和 5xx 有不同處理邏輯

REST vs GraphQL vs gRPC

特性RESTGraphQLgRPC
通訊協定HTTP/1.1+HTTP/1.1+HTTP/2
資料格式JSONJSONProtobuf
學習曲線
Over-fetching常見不會不會
即時通訊需 WebSocketSubscriptionStreaming
適用場景大多數 web API複雜前端、多資料來源微服務間通訊
瀏覽器支援完整完整需要 proxy
快取HTTP cache 直接用需要 client cache需自己做

實戰範例:一個完整的 API 回應

# Request
curl -X GET "https://api.example.com/v1/articles?page=2&per_page=10&sort=-published_at&category=tech" \
  -H "Authorization: Bearer eyJ..." \
  -H "Accept: application/json"
 
# Response (200 OK)
{
  "data": [
    {
      "id": "art_789",
      "title": "How to Design Good APIs",
      "slug": "how-to-design-good-apis",
      "category": "tech",
      "author": {
        "id": "usr_123",
        "name": "Terry"
      },
      "publishedAt": "2026-03-15T08:00:00Z",
      "readingTime": 8,
      "tags": ["api", "design", "rest"]
    }
  ],
  "pagination": {
    "page": 2,
    "perPage": 10,
    "totalPages": 15,
    "totalCount": 142,
    "hasNext": true,
    "hasPrev": true
  },
  "meta": {
    "requestId": "req_xyz789",
    "timestamp": "2026-03-15T10:30:00Z"
  }
}

FAQ

Q1: REST 和 RESTful 有什麼不一樣?

嚴格來說,REST 是 Roy Fielding 定義的架構風格(包含 HATEOAS 等嚴格條件),RESTful 是「大致遵循 REST 原則」的 API。99% 的所謂 REST API 其實是 RESTful API。不用太糾結名詞,把基本原則做好比較重要。

Q2: 該用 camelCase 還是 snake_case?

兩個都有人用。重點是全站統一。JavaScript/TypeScript 生態圈偏好 camelCase,Python/Ruby 偏好 snake_case。選一個就別換了。

Q3: 到底要不要用 Response Envelope?

推薦用。雖然有人覺得多一層包裝是多餘的,但它讓你可以統一附加 meta 資訊(pagination、request ID),前端處理起來也更一致。

Q4: API 版本什麼時候該升?

只有 breaking change 才升版號。加新欄位不算 breaking change(forward compatible),移除欄位或改變型別才是。

Q5: GraphQL 是不是比 REST 好?

不是「好不好」的問題,是「適不適合」。如果你的前端需要靈活組合不同資料、有很多 nested 資料需求,GraphQL 很適合。如果你做的是簡單的 CRUD API,REST 就夠了,不需要為了潮而上 GraphQL。


系列導航