【工程師的系統設計】#02 API 設計:打造系統的溝通契約

測驗:API 設計 — 打造系統的溝通契約

共 5 題,點選答案後會立即顯示結果

1. 在 RESTful API 設計中,URL 應該代表什麼?

  • A. 動作(例如 /getUsers/deleteUser
  • B. 資源(例如 /users/users/123
  • C. 功能模組(例如 /module/user-management
  • D. 版本號(例如 /v1/v2/endpoint

2. 為什麼 POST 方法被稱為「不冪等」?

  • A. 因為 POST 不需要傳送 body
  • B. 因為 POST 每次呼叫都可能失敗
  • C. 因為重複呼叫 POST 會建立多筆新資料,結果不同
  • D. 因為 POST 不支援 JSON 格式

3. 當 API 回傳 HTTP 狀態碼 401 時,代表什麼意思?

  • A. 請求格式不正確
  • B. 沒有登入(身份驗證失敗)
  • C. 有登入但沒有權限
  • D. 請求的資源不存在

4. 關於 API 版本控制,以下哪種修改「不需要」升版?

  • A. 新增一個選填的 query parameter
  • B. 刪除一個回應中的欄位
  • C. 改變某個欄位的資料型別
  • D. 重新命名一個必要參數

5. 在分頁設計中,什麼情況下 cursor-based 分頁比 offset-based 更適合?

  • A. 後台管理頁面需要跳到特定頁碼
  • B. 資料量很小(不到一千筆)
  • C. 使用者需要直接輸入頁碼瀏覽
  • D. 社群動態牆等資料持續新增的場景

系統裡的每個元件都需要「對話」,而 API 就是它們之間的共同語言。
設計好的 API,就像簽一份清楚的契約 — 雙方都知道該給什麼、會拿到什麼。

這篇文章解決什麼問題?

如果你有以下困惑,這篇文章適合你:

  • AI 幫你產生了一堆 API endpoint,但你不確定命名和結構對不對
  • 前端呼叫後端 API 時,不知道該用 POST 還是 PUT,回傳格式也不一致
  • 系統越做越大,舊的 API 改了會壞掉新功能,不知道怎麼管理版本

一句話說明

API 是系統元件之間的溝通契約,定義好「怎麼問、怎麼答」,系統才能穩定擴展。


API 在系統設計中的角色

在上一篇我們談了系統設計思維,知道系統是由多個元件組成的。那這些元件怎麼溝通?答案就是 API。

┌──────────┐     API      ┌──────────┐
│  前端 App │ ──────────→  │  後端 API │
└──────────┘              └──────────┘
                               │ API
                               ▼
                          ┌──────────┐
                          │  資料庫   │
                          └──────────┘

API 出現在三個關鍵位置:

場景 誰跟誰溝通 範例
前後端分離 瀏覽器 → 後端伺服器 React 呼叫 FastAPI
微服務通訊 服務 A → 服務 B 訂單服務 → 庫存服務
第三方整合 你的系統 → 外部服務 你的 App → Stripe 付款

重點:不管哪個場景,核心概念都一樣 — 雙方約定好「請求格式」和「回應格式」,這就是契約。


RESTful API 設計原則

REST 是目前最主流的 API 設計風格。你在大多數 AI 生成的後端程式碼中都會看到它。

原則 1:資源導向的 URL

URL 代表「資源」(名詞),不是「動作」(動詞)。

# 好的設計 -- URL 是名詞
GET    /users          # 取得所有使用者
GET    /users/123      # 取得 ID 為 123 的使用者
POST   /users          # 建立新使用者
PUT    /users/123      # 更新 ID 為 123 的使用者
DELETE /users/123      # 刪除 ID 為 123 的使用者

# 不好的設計 -- URL 是動詞
GET    /getUsers
POST   /createUser
POST   /deleteUser/123
Code language: PHP (php)

翻譯:把 URL 想成「對誰做事」– /users/123 就是「ID 123 的使用者」,至於「做什麼事」由 HTTP 方法決定。

原則 2:HTTP 方法的語義

每個 HTTP 方法都有固定的含義:

方法 做什麼 安全性 冪等性 白話翻譯
GET 讀取資料 安全(不改資料) 冪等(重複呼叫結果相同) 「給我看看」
POST 建立新資料 不安全 不冪等(重複呼叫會建多筆) 「幫我新增」
PUT 完整更新 不安全 冪等 「用這份覆蓋掉」
PATCH 部分更新 不安全 冪等 「只改這幾個欄位」
DELETE 刪除 不安全 冪等 「幫我刪掉」

什麼是冪等? 呼叫一次跟呼叫十次的結果一樣。DELETE /users/123 不管刪幾次,最後那筆都是被刪除的狀態。但 POST /users 呼叫十次就會建出十筆資料。

快速決策:PUT 還是 PATCH?

要更新資料,用 PUT 還是 PATCH?

Q1: 你要傳送完整的資源內容嗎?
├─ 是(全部欄位都傳)→ 用 PUT
└─ 否(只傳要改的欄位)→ 用 PATCH

實務上,大多數 AI 生成的程式碼都用 PATCH 做部分更新,這也是比較常見的做法。

原則 3:無狀態

每個請求都包含足夠的資訊,伺服器不需要「記住」之前的請求。

# 好的設計 -- 每個請求帶上身份驗證
GET /users/123
Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

# 不好的設計 -- 依賴伺服器記住「你是誰」
# 第一步:先登入(伺服器記住 session)
POST /login
# 第二步:靠 session 取資料
GET /users/123   # 伺服器從 session 查你是誰
Code language: PHP (php)

翻譯:就像去便利商店買東西,每次都要帶錢(token),不能說「我上次來過,老闆你應該記得」。


請求與回應設計

統一的 JSON 回應結構

好的 API 回應格式要一致。不管是取一筆還是取多筆,格式都可預期。

// 取得單筆資源:GET /users/123
{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "created_at": "2024-01-15T08:30:00Z"
}

// 取得多筆資源:GET /users
{
  "data": [
    {"id": 123, "name": "Alice", "email": "alice@example.com"},
    {"id": 124, "name": "Bob", "email": "bob@example.com"}
  ],
  "pagination": {
    "total": 50,
    "page": 1,
    "per_page": 20,
    "total_pages": 3
  }
}
Code language: JSON / JSON with Comments (json)

翻譯:多筆資料放在 data 陣列裡,分頁資訊放在 pagination。前端拿到就知道怎麼處理。

分頁:Offset vs Cursor

當資料量大時,不能一次全部回傳,需要分頁。兩種主流做法:

Offset-based(頁碼式) — 適合大部分情況:

GET /users?page=2&per_page=20

翻譯:「給我第 2 頁,每頁 20 筆」。簡單直覺,但資料量很大時效能會變差。

Cursor-based(游標式) — 適合大量資料或即時動態:

GET /users?cursor=eyJpZCI6MTIzfQ&limit=20

翻譯:「從上次的位置繼續往下給我 20 筆」。效能好,但沒辦法跳頁。

情境 建議選擇 原因
後台管理頁面 Offset 使用者需要跳到特定頁
社群動態牆 Cursor 資料持續新增,offset 會重複
資料量 < 10 萬 Offset 簡單好實作
資料量 > 百萬 Cursor Offset 在大量資料時會變慢

過濾與排序

讓前端可以彈性查詢:

# 過濾:只要活躍的使用者
GET /users?status=active

# 排序:按建立時間倒序
GET /users?sort=-created_at

# 組合使用
GET /users?status=active&sort=-created_at&page=1&per_page=20
Code language: PHP (php)

翻譯sort=-created_at- 代表降序(最新的在前面),沒有 - 就是升序。


錯誤處理模式

HTTP 狀態碼語義

狀態碼告訴你「這次請求的結果如何」:

2xx -- 成功
  200 OK          → 一般成功
  201 Created     → 成功建立新資源
  204 No Content  → 成功,但沒有要回傳的內容(常見於 DELETE)

4xx -- 用戶端錯誤(你的問題)
  400 Bad Request    → 請求格式不對
  401 Unauthorized   → 沒有登入(身份驗證失敗)
  403 Forbidden      → 有登入但沒有權限
  404 Not Found      → 資源不存在
  409 Conflict       → 資源衝突(例如 email 已被註冊)
  422 Unprocessable  → 格式對但內容不合理(例如年齡是負數)

5xx -- 伺服器錯誤(不是你的問題)
  500 Internal Server Error → 伺服器壞了
  503 Service Unavailable   → 伺服器暫時忙不過來
Code language: JavaScript (javascript)

快速判斷:看到 4xx 先檢查你的請求,看到 5xx 就是後端有問題。

統一錯誤格式

好的 API 不只回傳狀態碼,還會回傳結構化的錯誤訊息:

// 422 Unprocessable Entity
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "輸入資料驗證失敗",
    "details": [
      {
        "field": "email",
        "message": "email 格式不正確"
      },
      {
        "field": "age",
        "message": "年齡必須大於 0"
      }
    ]
  }
}
Code language: JSON / JSON with Comments (json)

這個結構的好處

  • code:程式可以用來判斷錯誤類型
  • message:人可以讀的描述
  • details:具體哪裡出錯,前端可以標示在對應的欄位上

API 版本控制

系統會成長,API 也需要改版。但舊的用戶端可能還在用舊版 API,怎麼辦?

兩種主流做法

URL Path Versioning(路徑版本) — 最常見:

GET /v1/users/123
GET /v2/users/123

Header Versioning(標頭版本)

GET /users/123
Headers: API-Version: 2
做法 優點 缺點 適合場景
URL 路徑 一眼就知道版本,好測試 URL 長了一節 公開 API、大部分情況
Header URL 乾淨 不容易看出版本 內部 API

實務建議:用 URL 路徑版本就對了。簡單、直覺、大家都看得懂。

什麼時候要升版?

不需要升版(向後相容的修改):
  - 新增欄位
  - 新增 endpoint
  - 新增可選的 query parameter

需要升版(破壞性修改):
  - 刪除或重新命名欄位
  - 改變欄位的資料型別
  - 改變必要參數
  - 改變回應結構

翻譯:「加東西」不用升版,「改東西」或「刪東西」才要。


認證與授權基礎

API 要保護起來,不是誰都能呼叫的。兩個核心概念:

  • 認證(Authentication):「你是誰?」– 證明你的身份
  • 授權(Authorization):「你能做什麼?」– 檢查你的權限

兩種常見方案

API Key — 簡單但功能有限:

GET /users
Headers: X-API-Key: sk_live_abc123def456

翻譯:就像一把鑰匙,有鑰匙就能進門。適合服務之間的通訊。

JWT Token — 目前最主流:

GET /users
Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjN9.xxx

翻譯:Token 裡面藏了「你是誰」的資訊,伺服器解開就知道。適合前後端分離的使用者認證。

方案 適合場景 特點
API Key 後端服務之間、第三方 API 簡單,不過期(除非主動撤銷)
JWT Token 使用者登入、前後端分離 帶有使用者資訊,會過期

實作範例:用 FastAPI 打造待辦事項 API

來看一個完整的範例,把以上所有原則用在一個「待辦事項 API」上。

後端:FastAPI 實作

from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel
from datetime import datetime

app = FastAPI()  # 建立 API 應用程式

# --- 資料模型(定義資料長什麼樣)---

class TodoCreate(BaseModel):  # 建立待辦事項時要傳什麼
    title: str                # 必填:標題(文字)
    description: str = ""     # 選填:描述(預設空字串)

class TodoUpdate(BaseModel):  # 更新時可以只傳要改的欄位
    title: str | None = None
    description: str | None = None
    completed: bool | None = None

class Todo(BaseModel):        # 完整的待辦事項結構
    id: int
    title: str
    description: str
    completed: bool
    created_at: datetime

class ErrorDetail(BaseModel):
    code: str
    message: str
    details: list = []

class ErrorResponse(BaseModel):  # 統一的錯誤格式
    error: ErrorDetail

# --- 模擬資料庫 ---
todos_db: dict[int, Todo] = {}
next_id = 1

# --- API Endpoints ---

# 取得所有待辦事項(支援過濾、分頁)
@app.get("/v1/todos")
def list_todos(
    completed: bool | None = None,           # 過濾:是否完成
    sort: str = "-created_at",               # 排序:預設最新在前
    page: int = Query(default=1, ge=1),      # 分頁:第幾頁(最小 1)
    per_page: int = Query(default=20, le=100) # 每頁幾筆(最多 100)
):
    items = list(todos_db.values())

    # 過濾
    if completed is not None:
        items = [t for t in items if t.completed == completed]

    # 排序
    reverse = sort.startswith("-")
    sort_field = sort.lstrip("-")
    items.sort(key=lambda t: getattr(t, sort_field), reverse=reverse)

    # 分頁
    total = len(items)
    start = (page - 1) * per_page
    items = items[start:start + per_page]

    return {
        "data": items,
        "pagination": {
            "total": total,
            "page": page,
            "per_page": per_page,
            "total_pages": (total + per_page - 1) // per_page
        }
    }

# 取得單筆待辦事項
@app.get("/v1/todos/{todo_id}")
def get_todo(todo_id: int):
    if todo_id not in todos_db:
        raise HTTPException(
            status_code=404,
            detail={
                "error": {
                    "code": "NOT_FOUND",
                    "message": f"待辦事項 {todo_id} 不存在"
                }
            }
        )
    return todos_db[todo_id]

# 建立待辦事項
@app.post("/v1/todos", status_code=201)  # 201 = Created
def create_todo(todo: TodoCreate):
    global next_id
    new_todo = Todo(
        id=next_id,
        title=todo.title,
        description=todo.description,
        completed=False,
        created_at=datetime.now()
    )
    todos_db[next_id] = new_todo
    next_id += 1
    return new_todo

# 部分更新待辦事項
@app.patch("/v1/todos/{todo_id}")
def update_todo(todo_id: int, updates: TodoUpdate):
    if todo_id not in todos_db:
        raise HTTPException(
            status_code=404,
            detail={
                "error": {
                    "code": "NOT_FOUND",
                    "message": f"待辦事項 {todo_id} 不存在"
                }
            }
        )
    existing = todos_db[todo_id]
    update_data = updates.model_dump(exclude_unset=True)  # 只取有傳的欄位
    updated = existing.model_copy(update=update_data)
    todos_db[todo_id] = updated
    return updated

# 刪除待辦事項
@app.delete("/v1/todos/{todo_id}", status_code=204)  # 204 = No Content
def delete_todo(todo_id: int):
    if todo_id not in todos_db:
        raise HTTPException(
            status_code=404,
            detail={
                "error": {
                    "code": "NOT_FOUND",
                    "message": f"待辦事項 {todo_id} 不存在"
                }
            }
        )
    del todos_db[todo_id]

逐行翻譯重點

@app.get("/v1/todos")            # 當有人用 GET 方法訪問 /v1/todos 時執行
def list_todos(
    completed: bool | None = None,  # 可選參數,FastAPI 自動從 ?completed=true 取值
    page: int = Query(default=1, ge=1),  # Query() 加上驗證:最小值 1
):
Code language: PHP (php)
raise HTTPException(status_code=404, detail={...})
# ↑ 拋出 HTTP 錯誤,FastAPI 會自動回傳 404 狀態碼 + 錯誤內容
Code language: PHP (php)
updates.model_dump(exclude_unset=True)
# ↑ 只取出「有傳的欄位」,沒傳的不算
# 例如只傳 {"completed": true},就只會更新 completed
Code language: PHP (php)
existing.model_copy(update=update_data)
# ↑ 用新資料覆蓋舊資料的對應欄位,其他欄位保持不變
Code language: PHP (php)

前端:React 消費 API

// api.js -- API 呼叫函式

const API_BASE = "/v1";

// 取得待辦事項列表(帶分頁和過濾)
async function listTodos({ page = 1, completed = null } = {}) {
  const params = new URLSearchParams({ page, per_page: 20 });
  if (completed !== null) params.set("completed", completed);

  const res = await fetch(`${API_BASE}/todos?${params}`);
  if (!res.ok) {
    const error = await res.json();    // 解析錯誤回應
    throw new Error(error.error.message);
  }
  return res.json();  // { data: [...], pagination: {...} }
}

// 建立待辦事項
async function createTodo(title, description = "") {
  const res = await fetch(`${API_BASE}/todos`, {
    method: "POST",                          // 建立用 POST
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title, description }),
  });
  if (!res.ok) {
    const error = await res.json();
    throw new Error(error.error.message);
  }
  return res.json();
}

// 更新待辦事項(部分更新)
async function updateTodo(id, updates) {
  const res = await fetch(`${API_BASE}/todos/${id}`, {
    method: "PATCH",                         // 部分更新用 PATCH
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(updates),           // 只傳要改的欄位
  });
  if (!res.ok) {
    const error = await res.json();
    throw new Error(error.error.message);
  }
  return res.json();
}

// 刪除待辦事項
async function deleteTodo(id) {
  const res = await fetch(`${API_BASE}/todos/${id}`, {
    method: "DELETE",
  });
  if (!res.ok && res.status !== 204) {
    const error = await res.json();
    throw new Error(error.error.message);
  }
  // DELETE 成功回傳 204,沒有 body
}
Code language: JavaScript (javascript)
// TodoList.jsx -- React 元件使用 API

import { useState, useEffect } from "react";

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [pagination, setPagination] = useState(null);
  const [page, setPage] = useState(1);

  // 載入待辦事項
  useEffect(() => {
    listTodos({ page }).then((result) => {
      setTodos(result.data);             // API 回傳的 data 陣列
      setPagination(result.pagination);  // API 回傳的分頁資訊
    });
  }, [page]);

  // 切換完成狀態
  async function toggleComplete(todo) {
    const updated = await updateTodo(todo.id, {
      completed: !todo.completed,        // 只傳 completed 欄位
    });
    setTodos(todos.map((t) => (t.id === updated.id ? updated : t)));
  }

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>
          <span>{todo.title}</span>
          <button onClick={() => toggleComplete(todo)}>
            {todo.completed ? "取消完成" : "完成"}
          </button>
        </div>
      ))}
      {pagination && (
        <div>
          第 {pagination.page} / {pagination.total_pages} 頁
          <button disabled={page <= 1} onClick={() => setPage(page - 1)}>
            上一頁
          </button>
          <button
            disabled={page >= pagination.total_pages}
            onClick={() => setPage(page + 1)}
          >
            下一頁
          </button>
        </div>
      )}
    </div>
  );
}
Code language: JavaScript (javascript)

這就是「契約」的體現:後端定義好 /v1/todos 回傳 { data, pagination } 的格式,前端就照這個格式解析。只要契約不變,前後端可以各自開發、各自部署。


常見坑與解法

坑 1:GET 請求帶 body

症狀:某些 HTTP 客戶端會忽略 GET 請求的 body,導致參數沒送到。

為什麼會發生:HTTP 規範允許 GET 帶 body,但不建議,很多工具不支援。

怎麼避免:GET 的參數一律放在 URL query string 裡。

# 正確
GET /users?status=active&page=2

# 不要這樣做
GET /users
Body: {"status": "active", "page": 2}
Code language: PHP (php)

坑 2:不一致的錯誤格式

症狀:有些 endpoint 回傳 {"error": "..."} 字串,有些回傳 {"message": "..."},前端不知道怎麼統一處理。

為什麼會發生:沒有在專案開始時定義統一的錯誤格式。

怎麼避免:在專案一開始就定義好錯誤回應的結構,全部 endpoint 都遵守。

跟 AI 說

「幫我建立一個 FastAPI 的統一錯誤處理機制,所有錯誤都回傳 {error: {code, message, details}} 格式」

坑 3:忘記處理分頁導致效能爆炸

症狀:API 越來越慢,因為一次回傳了幾萬筆資料。

為什麼會發生:開發初期資料少,沒加分頁。上線後資料量暴增。

怎麼避免:從第一天就加分頁,設定 per_page 的預設值和上限。

# 一開始就限制好
per_page: int = Query(default=20, le=100)  # 每頁最多 100 筆
Code language: PHP (php)

Vibe Coder 檢查點

看到 AI 生成的 API 程式碼時,確認以下事項:

  • [ ] URL 是名詞(資源)不是動詞(動作)嗎?
  • [ ] HTTP 方法用對了嗎?(GET 讀取、POST 建立、PATCH 更新、DELETE 刪除)
  • [ ] 有加分頁嗎?per_page 有設上限嗎?
  • [ ] 錯誤回應格式統一嗎?有用合適的 HTTP 狀態碼嗎?
  • [ ] 有版本號嗎?(/v1/ 前綴)
  • [ ] 需要認證的 endpoint 有加上驗證嗎?

AI 對話範例

入門對話

「幫我寫一個待辦事項的 API」

進階對話(更好的做法):

「幫我用 FastAPI 設計一個待辦事項 REST API。要求:
1. RESTful 風格,URL 用 /v1/todos
2. 支援 CRUD(GET/POST/PATCH/DELETE)
3. 列表 API 要有分頁(offset-based,每頁預設 20 筆,上限 100)
4. 支援 ?completed=true 過濾和 ?sort=-created_at 排序
5. 統一錯誤格式 {error: {code, message, details}}
6. 用 Pydantic model 定義請求和回應結構」

提供越多上下文,AI 產出的 API 設計就越符合最佳實踐。


小結

記住這個判斷框架

  1. URL 設計:名詞(資源),不是動詞(動作)
  2. HTTP 方法:GET 讀、POST 建、PATCH 改、DELETE 刪
  3. 回應格式:成功和錯誤都要有一致的結構
  4. 分頁:從第一天就加上,設好上限
  5. 版本控制:用 /v1/ 前綴,加東西不升版,改/刪才升版

最重要的原則:API 是契約,一旦發布就不能隨意更動。設計時多花十分鐘想清楚格式,後面少花十小時修 bug。


延伸:知道就好

這些進階主題遇到再深入:

  • GraphQL:另一種 API 風格,讓前端自己決定要哪些欄位。適合資料關係複雜的場景
  • gRPC:Google 的高效能 RPC 框架,適合微服務之間的通訊
  • OpenAPI / Swagger:API 文件自動生成工具,FastAPI 內建支援(訪問 /docs 就能看到)
  • Rate Limiting:限制 API 呼叫頻率,防止被濫用
  • HATEOAS:REST 的進階概念,API 回應中包含相關資源的連結

進階測驗:API 設計 — 打造系統的溝通契約

測驗目標:驗證你是否能在實際情境中應用 API 設計原則。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在設計一個電商系統的 API,需要讓前端取得某位使用者的訂單列表。以下哪個 endpoint 設計最符合 RESTful 原則? 情境題

  • A. POST /getOrdersByUser,在 body 中傳送 {"user_id": 123}
  • B. GET /orders/getUserOrders?id=123
  • C. GET /users/123/orders?page=1&per_page=20
  • D. GET /v1/query?type=orders&user=123

2. 你的待辦事項 API 已經上線,有很多前端在使用。現在產品經理要求把回應中的 created_at 欄位改名為 createdAt(駝峰命名)。你應該怎麼做? 情境題

  • A. 直接修改 created_atcreatedAt,通知前端更新
  • B. 發布新版本 /v2/todos,在新版本中使用 createdAt,舊版本保持不變
  • C. 同時回傳 created_atcreatedAt 兩個欄位,不需要升版
  • D. 用 Header Versioning 在同一個 URL 區分新舊格式

3. 你的系統需要同時支援「使用者登入」和「第三方服務呼叫」兩種認證場景。根據文章建議,最適合的認證方案組合是什麼? 情境題

  • A. 兩種場景都用 API Key
  • B. 兩種場景都用 JWT Token
  • C. 使用者登入用 API Key,第三方服務用 JWT Token
  • D. 使用者登入用 JWT Token,第三方服務用 API Key

4. 前端工程師回報:「呼叫 API 列表時越來越慢,最近一次花了 30 秒才回應。」你檢查了以下程式碼,問題最可能出在哪裡? 錯誤診斷

@app.get(“/v1/products”) def list_products( category: str | None = None, sort: str = “-created_at” ): items = list(products_db.values()) if category: items = [p for p in items if p.category == category] return {“data”: items}
  • A. 排序參數 sort 沒有被實際使用,導致回傳順序混亂
  • B. 沒有加分頁機制,當資料量增大時會一次回傳所有資料
  • C. 使用了 list() 轉換,應該直接回傳 dict.values()
  • D. 過濾條件 category 應該放在 URL 路徑而非 query parameter

5. 前端呼叫以下 API 更新待辦事項的標題,但發現 descriptioncompleted 欄位被清空了。這段程式碼哪裡出了問題? 錯誤診斷

# 前端請求 # PATCH /v1/todos/42 # Body: {“title”: “新標題”} # 後端處理 @app.patch(“/v1/todos/{todo_id}”) def update_todo(todo_id: int, updates: TodoUpdate): existing = todos_db[todo_id] update_data = updates.model_dump() # 取出所有欄位 updated = existing.model_copy(update=update_data) todos_db[todo_id] = updated return updated
  • A. 應該用 PUT 而不是 PATCH,因為 PATCH 不支援部分更新
  • B. model_copy() 方法不支援用 update 參數覆蓋欄位
  • C. model_dump() 應該加上 exclude_unset=True,否則沒傳的欄位會變成 None 覆蓋原值
  • D. 前端應該傳送完整的物件(包含所有欄位),不能只傳 title

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *