claude-code/claude-zh/skills/api-design/SKILL.md

524 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: api-design
description: REST API 設計模式,包含資源命名、狀態碼、分頁、篩選、錯誤回應、版本控制以及生產環境 API 的速率限制。
origin: ECC
---
# API 設計模式 (API Design Patterns)
設計一致且對開發者友好的 REST API 的慣例與最佳實踐。
## 何時啟用
- 設計新的 API 端點 (Endpoints)
- 評估現有的 API 合約
- 新增分頁、篩選或排序功能
- 為 API 實作錯誤處理
- 規劃 API 版本控制策略
- 構建面向公眾或合作夥伴的 API
## 資源設計 (Resource Design)
### URL 結構
```
# 資源應使用名詞、複數、小寫、kebab-case
GET /api/v1/users
GET /api/v1/users/:id
POST /api/v1/users
PUT /api/v1/users/:id
PATCH /api/v1/users/:id
DELETE /api/v1/users/:id
# 用於關聯的子資源
GET /api/v1/users/:id/orders
POST /api/v1/users/:id/orders
# 無法對應到 CRUD 的動作 (謹慎使用動詞)
POST /api/v1/orders/:id/cancel
POST /api/v1/auth/login
POST /api/v1/auth/refresh
```
### 命名規則
```
# 推薦 (GOOD)
/api/v1/team-members # 多單字資源使用 kebab-case
/api/v1/orders?status=active # 使用查詢參數進行篩選
/api/v1/users/123/orders # 使用巢狀資源表示歸屬關係
# 不推薦 (BAD)
/api/v1/getUsers # URL 中包含動詞
/api/v1/user # 使用單數 (應使用複數)
/api/v1/team_members # URL 中使用 snake_case
/api/v1/users/123/getOrders # 巢狀資源中包含動詞
```
## HTTP 方法與狀態碼
### 方法語意 (Method Semantics)
| 方法 | 冪等性 (Idempotent) | 安全性 (Safe) | 用途 |
|--------|-----------|------|---------|
| GET | 是 | 是 | 檢索資源 |
| POST | 否 | 否 | 建立資源、觸發動作 |
| PUT | 是 | 否 | 完整替換資源 |
| PATCH | 否* | 否 | 部分更新資源 |
| DELETE | 是 | 否 | 移除資源 |
*PATCH 若有正確實作也可以具備冪等性。
### 狀態碼參考
```
# 成功 (Success)
200 OK — GET, PUT, PATCH (含回應主體)
201 Created — POST (應包含 Location 標頭)
204 No Content — DELETE, PUT (無回應主體)
# 客戶端錯誤 (Client Errors)
400 Bad Request — 驗證失敗、JSON 格式錯誤
401 Unauthorized — 缺失或無效的身分驗證
403 Forbidden — 已驗證但無權限
404 Not Found — 資源不存在
409 Conflict — 重複輸入、狀態衝突
422 Unprocessable Entity — 語法正確但語意無效 (JSON 合法,數據有誤)
429 Too Many Requests — 超過速率限制
# 伺服器錯誤 (Server Errors)
500 Internal Server Error — 非預期失敗 (絕對不要暴露詳細細節)
502 Bad Gateway — 上游服務失敗
503 Service Unavailable — 暫時性超載,應包含 Retry-After
```
### 常見錯誤
```
# 錯誤範例 (BAD):不論什麼情況都回傳 200
{ "status": 200, "success": false, "error": "Not found" }
# 正確案例 (GOOD):語意化地使用 HTTP 狀態碼
HTTP/1.1 404 Not Found
{ "error": { "code": "not_found", "message": "User not found" } }
# 錯誤範例 (BAD):針對驗證錯誤回傳 500
# 正確案例 (GOOD):回傳 400 或 422 並提供欄位級別的詳細資訊
# 錯誤範例 (BAD):針對建立的資源回傳 200
# 正確案例 (GOOD):回傳 201 並附帶 Location 標頭
HTTP/1.1 201 Created
Location: /api/v1/users/abc-123
```
## 回應格式 (Response Format)
### 成功回應
```json
{
"data": {
"id": "abc-123",
"email": "alice@example.com",
"name": "Alice",
"created_at": "2025-01-15T10:30:00Z"
}
}
```
### 集合回應 (包含分頁)
```json
{
"data": [
{ "id": "abc-123", "name": "Alice" },
{ "id": "def-456", "name": "Bob" }
],
"meta": {
"total": 142,
"page": 1,
"per_page": 20,
"total_pages": 8
},
"links": {
"self": "/api/v1/users?page=1&per_page=20",
"next": "/api/v1/users?page=2&per_page=20",
"last": "/api/v1/users?page=8&per_page=20"
}
}
```
### 錯誤回應
```json
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"code": "invalid_format"
},
{
"field": "age",
"message": "Must be between 0 and 150",
"code": "out_of_range"
}
]
}
}
```
### 回應外殼 (Envelope) 變體
```typescript
// 選項 A帶有數據包裝的外殼 (建議用於公眾 API)
interface ApiResponse<T> {
data: T;
meta?: PaginationMeta;
links?: PaginationLinks;
}
interface ApiError {
error: {
code: string;
message: string;
details?: FieldError[];
};
}
// 選項 B扁平化回應 (較簡單,常見於內部 API)
// 成功:直接回傳資源
// 錯誤:回傳錯誤物件
// 透過 HTTP 狀態碼進行區分
```
## 分頁 (Pagination)
### 基於偏移量 (Offset-Based簡單)
```
GET /api/v1/users?page=2&per_page=20
# 實作概念
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 20;
```
**點:** 易於實作,支援「跳轉到第 N 頁」。
**缺點:** 對於大型偏移量 (OFFSET 100000) 效能較慢,且在併發插入時會出現不一致的情況。
### 基於游標 (Cursor-Based具擴展性)
```
GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
# 實作概念
SELECT * FROM users
WHERE id > :cursor_id
ORDER BY id ASC
LIMIT 21; -- 多抓一個以判斷是否有下一頁 (has_next)
```
```json
{
"data": [...],
"meta": {
"has_next": true,
"next_cursor": "eyJpZCI6MTQzfQ"
}
}
```
**優點:** 不論位置如何效能皆保持一致,且在併發插入時表現穩定。
**缺點:** 無法跳轉到任意頁碼,且游標內容不具備可讀性。
### 該選用哪一種?
| 使用情境 | 分頁類型 |
|----------|----------------|
| 管理後台、小型資料集 (<10K) | 偏移量 (Offset) |
| 無限滾動動態牆大型資料集 | 游標 (Cursor) |
| 公眾 API | 預設游標 (可選配偏移量) |
| 搜尋結果 | 偏移量 (使用者預期看到頁碼) |
## 篩選、排序與搜尋
### 篩選 (Filtering)
```
# 簡單等值
GET /api/v1/orders?status=active&customer_id=abc-123
# 比較運算子 (使用括號表示)
GET /api/v1/products?price[gte]=10&price[lte]=100
GET /api/v1/orders?created_at[after]=2025-01-01
# 多重數值 (逗號分隔)
GET /api/v1/products?category=electronics,clothing
# 巢狀欄位 (點表示法)
GET /api/v1/orders?customer.country=US
```
### 排序 (Sorting)
```
# 單一欄位 (前綴 - 表示降冪)
GET /api/v1/products?sort=-created_at
# 多個欄位 (逗號分隔)
GET /api/v1/products?sort=-featured,price,-created_at
```
### 全文檢索 (Full-Text Search)
```
# 搜尋查詢參數
GET /api/v1/products?q=wireless+headphones
# 特定欄位搜尋
GET /api/v1/users?email=alice
```
### 稀疏欄位集 (Sparse Fieldsets)
```
# 僅返回指定欄位 (減少回應體體積)
GET /api/v1/users?fields=id,name,email
GET /api/v1/orders?fields=id,total,status&include=customer.name
```
## 身分驗證與授權
### 基於 Token 的驗證
```
# Authorization 標頭中的 Bearer token
GET /api/v1/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# API key (用於伺服器對伺服器通訊)
GET /api/v1/data
X-API-Key: sk_live_abc123
```
### 授權模式 (Authorization Patterns)
```typescript
// 資源級別:檢查所有權
app.get("/api/v1/orders/:id", async (req, res) => {
const order = await Order.findById(req.params.id);
if (!order) return res.status(404).json({ error: { code: "not_found" } });
if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
return res.json({ data: order });
});
// 基於角色:檢查權限
app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
await User.delete(req.params.id);
return res.status(204).send();
});
```
## 速率限制 (Rate Limiting)
### 標頭 (Headers)
```
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000
# 超過限制時
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again in 60 seconds."
}
}
```
### 速率限制分層
| 層級 | 限制 | 視窗 | 使用情境 |
|------|-------|--------|----------|
| 匿名 | 30/min | IP | 公眾端點 |
| 已驗證 | 100/min | 每使用者 | 標準 API 存取 |
| 高級 (Premium) | 1000/min | API key | 付費 API 方案 |
| 內部 | 10000/min | 每服務 | 服務間通訊 |
## 版本控制 (Versioning)
### URL 路徑版本控制 (建議方案)
```
/api/v1/users
/api/v2/users
```
**優點:** 明確易於路由可緩存
**缺點:** 不同版本間 URL 會更動
### 標頭版本控制 (Header Versioning)
```
GET /api/users
Accept: application/vnd.myapp.v2+json
```
**優點:** URL 保持乾淨
**缺點:** 較難測試容易遺忘
### 版本控制策略
```
1. 以 /api/v1/ 開始 — 在有需求之前不要進行版本控制。
2. 最多維持 2 個活動版本 (當前版 + 前一版)。
3. 棄用 (Deprecation) 時間表:
- 宣佈棄用 (公眾 API 提前 6 個月通知)。
- 加入 Sunset 標頭Sunset: Sat, 01 Jan 2026 00:00:00 GMT。
- 棄用日期後,回傳 410 Gone。
4. 非破壞性變更不需要新版本:
- 在回應中新增欄位。
- 新增可選的查詢參數。
- 新增端點。
5. 破壞性變更需要新版本:
- 移除或重新命名欄位。
- 更改欄位型別。
- 更改 URL 結構。
- 更改身分驗證方法。
```
## 實作模式
### TypeScript (Next.js API Route)
```typescript
import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
});
export async function POST(req: NextRequest) {
const body = await req.json();
const parsed = createUserSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({
error: {
code: "validation_error",
message: "Request validation failed",
details: parsed.error.issues.map(i => ({
field: i.path.join("."),
message: i.message,
code: i.code,
})),
},
}, { status: 422 });
}
const user = await createUser(parsed.data);
return NextResponse.json(
{ data: user },
{
status: 201,
headers: { Location: `/api/v1/users/${user.id}` },
},
);
}
```
### Python (Django REST Framework)
```python
from rest_framework import serializers, viewsets, status
from rest_framework.response import Response
class CreateUserSerializer(serializers.Serializer):
email = serializers.EmailField()
name = serializers.CharField(max_length=100)
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "email", "name", "created_at"]
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
if self.action == "create":
return CreateUserSerializer
return UserSerializer
def create(self, request):
serializer = CreateUserSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = UserService.create(**serializer.validated_data)
return Response(
{"data": UserSerializer(user).data},
status=status.HTTP_201_CREATED,
headers={"Location": f"/api/v1/users/{user.id}"},
)
```
### Go (net/http)
```go
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
return
}
if err := req.Validate(); err != nil {
writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
return
}
user, err := h.service.Create(r.Context(), req)
if err != nil {
switch {
case errors.Is(err, domain.ErrEmailTaken):
writeError(w, http.StatusConflict, "email_taken", "Email already registered")
default:
writeError(w, http.StatusInternalServerError, "internal_error", "Internal error")
}
return
}
w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID))
writeJSON(w, http.StatusCreated, map[string]any{"data": user})
}
```
## API 設計檢核清單
在交付新端點之前
- [ ] 資源 URL 遵循命名規範 (複數kebab-case不含動詞)
- [ ] 使用正確的 HTTP 方法 (GET 用於讀取POST 用於建立等)
- [ ] 回傳適當的狀態碼 (不論什麼情況都回傳 200 是錯誤的)
- [ ] 輸入經過 Schema 驗證 (Zod, Pydantic, Bean Validation)
- [ ] 錯誤回應遵循標準格式包含代碼與訊息
- [ ] 對於清單端點已實作分頁 (游標或偏移量)
- [ ] 要求身分驗證 (或明確標註為公眾存取)
- [ ] 已檢查授權 (使用者僅能存取自己的資源)
- [ ] 已配置速率限制
- [ ] 回應中不會洩漏內部細節 (堆疊追蹤SQL 錯誤等)
- [ ] 與現有端點的命名方式保持一致 (camelCase vs snake_case)
- [ ] 已撰寫文件 (OpenAPI/Swagger 規範已更新)