測驗:FastAPI 完整 CRUD API 實作
共 5 題,點選答案後會立即顯示結果
1. 在 RESTful API 設計中,要新增一個待辦事項應該使用哪個 HTTP 方法?
2. 為什麼 FastAPI 專案通常會將資料模型分成 TodoCreate、TodoUpdate 和 Todo 三個類別?
3. 以下程式碼中,status_code=status.HTTP_201_CREATED 的用途是什麼?
@app.post(“/todos”, response_model=Todo, status_code=status.HTTP_201_CREATED)
def create_todo(todo: TodoCreate):
…
4. 當使用 PATCH 方法進行部分更新時,todo.model_dump(exclude_unset=True) 的作用是什麼?
5. 刪除資源成功後,應該回傳哪個 HTTP 狀態碼?
前言
經過前四篇的學習,你已經掌握了 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)使用常數比直接寫數字更易讀、更不容易出錯。
重點整理
- CRUD 對應 HTTP 方法
- Create → POST
- Read → GET
- Update → PUT(完整)/ PATCH(部分)
- Delete → DELETE
- 適當的狀態碼
- 201:建立成功
- 204:刪除成功(無內容)
- 404:找不到資源
- HTTPException 處理錯誤
- 提供明確的錯誤訊息
- 使用適當的狀態碼
- 分離資料模型
- 輸入模型(Create/Update)
- 輸出模型(完整資料)
系列總結
恭喜你完成了 FastAPI 教學系列!讓我們回顧一下學到的內容:
- #01 認識 FastAPI 專案結構:專案檔案組成與基本架構
- #02 路由與路徑參數:處理不同 URL 與動態參數
- #03 查詢參數與請求處理:篩選、分頁等查詢功能
- #04 Pydantic 資料驗證:確保資料正確性與型別安全
- #05 完整 CRUD API 實作:整合所有知識的實戰應用
有了這些基礎,你已經能夠讀懂大部分 FastAPI 專案的程式碼。當 AI 生成 FastAPI 程式碼時,你可以理解每一行在做什麼,並在需要時進行修改。
延伸學習
如果想進一步學習,可以探索:
- 資料庫整合(SQLAlchemy、SQLModel)
- 使用者認證(OAuth2、JWT)
- 背景任務(Background Tasks)
- WebSocket 即時通訊
- 檔案上傳處理
進階測驗:FastAPI 完整 CRUD API 實作
測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。
共 5 題,包含情境題與錯誤診斷題。
1. 你正在開發一個待辦事項 API,需要實作「只更新使用者有提供的欄位」功能。使用者只傳了 {"completed": true},你希望保留原本的 title 不變。應該使用哪種方式? 情境題
2. 小明寫了以下程式碼來取得單一待辦事項,但當使用者查詢不存在的 id 時,API 回傳 500 錯誤而不是 404。問題出在哪裡? 錯誤診斷
@app.get(“/todos/{todo_id}”, response_model=Todo)
def get_todo(todo_id: int):
return todos[todo_id]
3. 你的 API 新增待辦事項成功後,目前回傳 HTTP 200 OK。PM 希望讓前端更容易判斷「資源是新建立的」。應該如何改進? 情境題
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 {}