【FastAPI 教學】#05 完整 CRUD API 實作

測驗:FastAPI 完整 CRUD API 實作

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

1. 在 RESTful API 設計中,要新增一個待辦事項應該使用哪個 HTTP 方法?

  • A. GET
  • B. POST
  • C. PUT
  • D. DELETE

2. 為什麼 FastAPI 專案通常會將資料模型分成 TodoCreateTodoUpdateTodo 三個類別?

  • A. 這是 Python 的強制規定
  • B. 為了讓程式碼看起來更複雜
  • C. 因為新增時不需要 id、更新時所有欄位應為可選、回應時需要完整資料
  • D. 為了讓資料庫查詢更快速

3. 以下程式碼中,status_code=status.HTTP_201_CREATED 的用途是什麼?

@app.post(“/todos”, response_model=Todo, status_code=status.HTTP_201_CREATED) def create_todo(todo: TodoCreate): …
  • A. 當發生錯誤時回傳 201 狀態碼
  • B. 當資源成功建立時回傳 201 狀態碼
  • C. 讓 API 只接受 201 次請求
  • D. 設定請求的逾時時間為 201 毫秒

4. 當使用 PATCH 方法進行部分更新時,todo.model_dump(exclude_unset=True) 的作用是什麼?

  • A. 只包含使用者實際提供的欄位,排除未設定的欄位
  • B. 將所有欄位都設為 None
  • C. 刪除所有預設值的欄位
  • D. 只保留 id 欄位

5. 刪除資源成功後,應該回傳哪個 HTTP 狀態碼?

  • A. 200 OK
  • B. 201 Created
  • C. 204 No Content
  • D. 404 Not Found

前言

經過前四篇的學習,你已經掌握了 FastAPI 的核心概念:路由、路徑參數、查詢參數,以及 Pydantic 資料驗證。現在是時候把這些知識整合起來,實作一個完整的 CRUD API 了。

CRUD 是 Create(新增)、Read(讀取)、Update(更新)、Delete(刪除)的縮寫,是所有後端 API 的基礎操作。當你在 AI 生成的程式碼中看到這些操作時,理解它們的運作方式至關重要。

學習目標

讀完本篇後,你將能夠:

  • 實作完整的 CRUD(Create, Read, Update, Delete)API
  • 使用適當的 HTTP 方法與狀態碼
  • 處理錯誤情況(HTTPException)

RESTful API 設計原則

在閱讀 API 程式碼前,先了解 RESTful 的設計慣例:

操作 HTTP 方法 路徑範例 說明
列出全部 GET /items 取得所有項目
取得單一 GET /items/{id} 取得特定項目
新增 POST /items 建立新項目
完整更新 PUT /items/{id} 替換整個項目
部分更新 PATCH /items/{id} 更新部分欄位
刪除 DELETE /items/{id} 刪除項目

完整範例:待辦事項 API

讓我們透過一個待辦事項(Todo)API 來學習完整的 CRUD 實作:

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

# 資料模型
class TodoCreate(BaseModel):
    title: str
    completed: bool = False

class TodoUpdate(BaseModel):
    title: Optional[str] = None
    completed: Optional[bool] = None

class Todo(BaseModel):
    id: int
    title: str
    completed: bool

# 模擬資料庫(記憶體儲存)
todos: dict[int, Todo] = {}
next_id = 1

讀懂資料模型設計

這段程式碼定義了三個不同用途的模型:

class TodoCreate(BaseModel):  # 新增時使用
    title: str
    completed: bool = False

class TodoUpdate(BaseModel):  # 更新時使用
    title: Optional[str] = None
    completed: Optional[bool] = None

class Todo(BaseModel):        # 完整資料(含 id)
    id: int
    title: str
    completed: bool

為什麼要分開?

  • TodoCreate:新增時不需要 id(由系統產生)
  • TodoUpdate:更新時所有欄位都是可選的
  • Todo:完整資料結構,用於回應

Create(新增):POST 方法

@app.post("/todos", response_model=Todo, status_code=status.HTTP_201_CREATED)
def create_todo(todo: TodoCreate):
    global next_id
    new_todo = Todo(id=next_id, **todo.model_dump())
    todos[next_id] = new_todo
    next_id += 1
    return new_todo
Code language: PHP (php)

逐行解析

@app.post("/todos", response_model=Todo, status_code=status.HTTP_201_CREATED)
Code language: JavaScript (javascript)
  • @app.post:處理 POST 請求
  • response_model=Todo:指定回應的資料結構
  • statuscode=status.HTTP201_CREATED:成功時回傳 201(資源已建立)
def create_todo(todo: TodoCreate):
  • 參數 todo: TodoCreate 會自動從請求 body 解析 JSON
new_todo = Todo(id=next_id, **todo.model_dump())
  • todo.model_dump() 將 Pydantic 模型轉為字典
  • ** 解包字典,等同於 Todo(id=next_id, title=todo.title, completed=todo.completed)

Read(讀取):GET 方法

列出全部

@app.get("/todos", response_model=list[Todo])
def list_todos():
    return list(todos.values())
Code language: PHP (php)
  • response_model=list[Todo]:回傳 Todo 物件的列表
  • todos.values() 取得字典中所有值

取得單一項目

@app.get("/todos/{todo_id}", response_model=Todo)
def get_todo(todo_id: int):
    if todo_id not in todos:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Todo with id {todo_id} not found"
        )
    return todos[todo_id]
Code language: JavaScript (javascript)

HTTPException 錯誤處理

raise HTTPException(
    status_code=status.HTTP_404_NOT_FOUND,
    detail=f"Todo with id {todo_id} not found"
)
Code language: JavaScript (javascript)

當找不到資料時,使用 HTTPException 回傳適當的錯誤:

  • status_code:HTTP 狀態碼(404 表示找不到資源)
  • detail:錯誤訊息,會包含在回應的 JSON 中

回應範例:

{
    "detail": "Todo with id 999 not found"
}
Code language: JSON / JSON with Comments (json)

Update(更新):PUT 與 PATCH 方法

PUT:完整更新

@app.put("/todos/{todo_id}", response_model=Todo)
def update_todo(todo_id: int, todo: TodoCreate):
    if todo_id not in todos:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Todo with id {todo_id} not found"
        )
    updated_todo = Todo(id=todo_id, **todo.model_dump())
    todos[todo_id] = updated_todo
    return updated_todo
Code language: JavaScript (javascript)

PUT 方法會完整替換資源,需要提供所有必要欄位。

PATCH:部分更新

@app.patch("/todos/{todo_id}", response_model=Todo)
def partial_update_todo(todo_id: int, todo: TodoUpdate):
    if todo_id not in todos:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Todo with id {todo_id} not found"
        )

    existing_todo = todos[todo_id]
    update_data = todo.model_dump(exclude_unset=True)

    updated_todo = existing_todo.model_copy(update=update_data)
    todos[todo_id] = updated_todo
    return updated_todo
Code language: PHP (php)

關鍵技巧解析

update_data = todo.model_dump(exclude_unset=True)
Code language: PHP (php)
  • exclude_unset=True:只包含使用者實際提供的欄位
  • 例如只傳 {"completed": true},則 update_data = {"completed": True}
updated_todo = existing_todo.model_copy(update=update_data)
  • model_copy(update=...):複製現有物件並更新指定欄位
  • 保留原有的 id 和未更新的欄位

Delete(刪除):DELETE 方法

@app.delete("/todos/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_todo(todo_id: int):
    if todo_id not in todos:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Todo with id {todo_id} not found"
        )
    del todos[todo_id]
    return None
Code language: JavaScript (javascript)

關鍵點

  • statuscode=status.HTTP204NOCONTENT:刪除成功回傳 204
  • 204 表示「成功但無內容回傳」
  • return None:不回傳任何資料

HTTP 狀態碼速查

狀態碼 名稱 使用時機
200 OK 一般成功回應
201 Created 資源建立成功
204 No Content 成功但無回傳內容(常用於刪除)
400 Bad Request 請求格式錯誤
404 Not Found 資源不存在
422 Unprocessable Entity 驗證錯誤(FastAPI 預設)

實際測試

啟動伺服器後,可用以下方式測試:

新增待辦事項

curl -X POST http://localhost:8000/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "學習 FastAPI"}'
Code language: JavaScript (javascript)

回應(201 Created):

{"id": 1, "title": "學習 FastAPI", "completed": false}
Code language: JSON / JSON with Comments (json)

列出所有待辦事項

curl http://localhost:8000/todos
Code language: JavaScript (javascript)

部分更新

curl -X PATCH http://localhost:8000/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'
Code language: JavaScript (javascript)

刪除

curl -X DELETE http://localhost:8000/todos/1
Code language: JavaScript (javascript)

常見程式碼模式

當你在閱讀 AI 生成的 CRUD 程式碼時,留意這些模式:

模式 1:資源不存在檢查

if item_id not in items:
    raise HTTPException(status_code=404, detail="Item not found")
Code language: JavaScript (javascript)

幾乎每個需要操作特定資源的端點都會有這個檢查。

模式 2:分離輸入/輸出模型

class ItemCreate(BaseModel): ...  # 輸入
class ItemUpdate(BaseModel): ...  # 更新輸入
class Item(BaseModel): ...        # 輸出

這是 FastAPI 推薦的最佳實踐,讓 API 更清晰。

模式 3:狀態碼常數

from fastapi import status

status.HTTP_200_OK
status.HTTP_201_CREATED
status.HTTP_204_NO_CONTENT
status.HTTP_404_NOT_FOUND
Code language: CSS (css)

使用常數比直接寫數字更易讀、更不容易出錯。

重點整理

  1. CRUD 對應 HTTP 方法
    • Create → POST
    • Read → GET
    • Update → PUT(完整)/ PATCH(部分)
    • Delete → DELETE
  2. 適當的狀態碼
    • 201:建立成功
    • 204:刪除成功(無內容)
    • 404:找不到資源
  3. HTTPException 處理錯誤
    • 提供明確的錯誤訊息
    • 使用適當的狀態碼
  4. 分離資料模型
    • 輸入模型(Create/Update)
    • 輸出模型(完整資料)

系列總結

恭喜你完成了 FastAPI 教學系列!讓我們回顧一下學到的內容:

  1. #01 認識 FastAPI 專案結構:專案檔案組成與基本架構
  2. #02 路由與路徑參數:處理不同 URL 與動態參數
  3. #03 查詢參數與請求處理:篩選、分頁等查詢功能
  4. #04 Pydantic 資料驗證:確保資料正確性與型別安全
  5. #05 完整 CRUD API 實作:整合所有知識的實戰應用

有了這些基礎,你已經能夠讀懂大部分 FastAPI 專案的程式碼。當 AI 生成 FastAPI 程式碼時,你可以理解每一行在做什麼,並在需要時進行修改。

延伸學習

如果想進一步學習,可以探索:

  • 資料庫整合(SQLAlchemy、SQLModel)
  • 使用者認證(OAuth2、JWT)
  • 背景任務(Background Tasks)
  • WebSocket 即時通訊
  • 檔案上傳處理

進階測驗:FastAPI 完整 CRUD API 實作

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

1. 你正在開發一個待辦事項 API,需要實作「只更新使用者有提供的欄位」功能。使用者只傳了 {"completed": true},你希望保留原本的 title 不變。應該使用哪種方式? 情境題

  • A. 使用 PUT 方法,要求使用者提供所有欄位
  • B. 使用 POST 方法建立新的待辦事項
  • C. 使用 PATCH 方法搭配 model_dump(exclude_unset=True)model_copy(update=...)
  • D. 使用 DELETE 方法先刪除再重新建立

2. 小明寫了以下程式碼來取得單一待辦事項,但當使用者查詢不存在的 id 時,API 回傳 500 錯誤而不是 404。問題出在哪裡? 錯誤診斷

@app.get(“/todos/{todo_id}”, response_model=Todo) def get_todo(todo_id: int): return todos[todo_id]
  • A. 缺少 response_model 設定
  • B. 沒有檢查 todo_id 是否存在,應該在不存在時 raise HTTPException(status_code=404)
  • C. 應該使用 POST 方法而不是 GET
  • D. todo_id 參數型別應該改為 str

3. 你的 API 新增待辦事項成功後,目前回傳 HTTP 200 OK。PM 希望讓前端更容易判斷「資源是新建立的」。應該如何改進? 情境題

  • A. 在回應 body 中加入 "status": "created" 欄位
  • B. 將狀態碼改為 HTTP 201 Created,使用 status_code=status.HTTP_201_CREATED
  • C. 使用 PUT 方法取代 POST 方法
  • D. 回傳 HTTP 204 No Content

4. 小華寫了以下刪除 API,但前端反映刪除成功後收到空的 JSON 回應導致解析錯誤。如何修正? 錯誤診斷

@app.delete(“/todos/{todo_id}”) def delete_todo(todo_id: int): if todo_id not in todos: raise HTTPException(status_code=404, detail=”Todo not found”) del todos[todo_id] return {}
  • A. 應該回傳刪除的項目內容
  • B. 應該使用 POST 方法進行刪除
  • C. 設定 status_code=status.HTTP_204_NO_CONTENTreturn None,讓前端知道不會有回應內容
  • D. 將 HTTPException 的 status_code 改為 204

5. 你需要設計一個 API 來更新待辦事項。前端有兩種需求:(1) 表單編輯頁面會送出完整資料 (2) 快速勾選完成只送 completed 欄位。最佳做法是? 情境題

  • A. 只實作 PUT 方法,要求前端每次都送完整資料
  • B. 只實作 PATCH 方法,讓前端決定要送哪些欄位
  • C. 建立兩個不同的 POST 端點處理不同需求
  • D. 同時實作 PUT(完整更新)和 PATCH(部分更新),讓前端根據情境選用

發佈留言

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