FastAPI
單元:路由(Routing)
主題:支援的 HTTP 方法(GET, POST, PUT, PATCH, DELETE, OPTIONS)
簡介
在 Web API 開發中,**路由(Routing)**是把客戶端的請求對應到程式碼的關鍵機制。FastAPI 以 ASGI 為底層,提供了直覺且型別安全的路由宣告方式,讓開發者可以輕鬆定義支援的 HTTP 方法(GET、POST、PUT、PATCH、DELETE、OPTIONS)以及相關的參數驗證。
掌握每個方法的語意與正確使用時機,能讓 API 符合 RESTful 原則、提升可讀性與維護性,同時也有助於前端或第三方服務的整合。本文將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你建立一套完整且可擴充的 FastAPI 路由。
核心概念
1. HTTP 方法的語意
| 方法 | 語意 | 常見用途 |
|---|---|---|
| GET | 取得資源,不應該改變伺服器狀態 | 查詢列表、單筆資料 |
| POST | 建立新資源或觸發非冪等操作 | 新增資料、上傳檔案 |
| PUT | 完全取代目標資源 | 更新全部欄位 |
| PATCH | 部分更新目標資源 | 只改變部份欄位 |
| DELETE | 刪除資源 | 移除資料 |
| OPTIONS | 取得目標 URL 支援的 HTTP 方法(CORS 預檢) | 前端跨域請求的預檢 |
FastAPI 直接以裝飾器(@app.get()、@app.post()…)對應上述方法,並且可以同時宣告 路徑參數、查詢參數、請求體 與 回應模型。
2. 基本路由宣告
from fastapi import FastAPI
app = FastAPI()
@app.get("/ping")
async def ping() -> str:
"""簡單的健康檢查,回傳字串"""
return "pong"
@app.get("/ping"):宣告此路由只接受 GET 請求。async def:建議使用 非阻塞 的 async 函式,以發揮 FastAPI 的高效能。
3. 使用 Pydantic 定義請求與回應模型
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
description: str | None = None
BaseModel讓資料結構在 驗證 與 自動文件化(OpenAPI)上都有保障。
程式碼範例
以下示範 GET、POST、PUT、PATCH、DELETE、OPTIONS 六種方法的實作,涵蓋路徑參數、查詢參數、請求體與回應模型。
3.1 GET:取得單筆與多筆資料
from fastapi import FastAPI, Query
from typing import List
app = FastAPI()
# 假資料庫
FAKE_DB = {
1: {"name": "Apple", "price": 3.5},
2: {"name": "Banana", "price": 2.0},
}
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: int) -> Item:
"""
依據 item_id 取得單筆商品資訊。
若不存在則拋出 404。
"""
data = FAKE_DB.get(item_id)
if not data:
raise HTTPException(status_code=404, detail="Item not found")
return Item(**data)
@app.get("/items", response_model=List[Item])
async def list_items(skip: int = Query(0, ge=0), limit: int = Query(10, le=100)):
"""
取得商品清單,支援分頁參數 `skip` 與 `limit`。
"""
items = [Item(**v) for v in list(FAKE_DB.values())[skip : skip + limit]]
return items
@app.get("/items/{item_id}"):路徑參數item_id必須是 int,FastAPI 會自動驗證。Query用於 查詢字串(如?skip=0&limit=10),可設定預設值與驗證條件。
3.2 POST:建立新資源
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
next_id = 3 # 模擬自動遞增的主鍵
@app.post(
"/items",
response_model=Item,
status_code=status.HTTP_201_CREATED,
)
async def create_item(item: Item) -> Item:
"""
接收 JSON 請求體,建立新商品並回傳建立後的資料。
"""
global next_id
if any(v["name"] == item.name for v in FAKE_DB.values()):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Item already exists",
)
FAKE_DB[next_id] = item.dict()
item_id = next_id
next_id += 1
return Item(**FAKE_DB[item_id])
@app.post("/items"):只接受 POST。status_code=status.HTTP_201_CREATED:符合 REST 建立資源的慣例。item: Item讓 FastAPI 自動解析 JSON 並驗證。
3.3 PUT:完整取代資源
@app.put("/items/{item_id}", response_model=Item)
async def replace_item(item_id: int, item: Item) -> Item:
"""
用提供的完整資料 *完全* 取代指定的商品。
若目標不存在則回傳 404。
"""
if item_id not in FAKE_DB:
raise HTTPException(status_code=404, detail="Item not found")
FAKE_DB[item_id] = item.dict()
return Item(**FAKE_DB[item_id])
PUT要求 完整 的資源表示;若缺少欄位,應視為錯誤或使用預設值。
3.4 PATCH:部份更新
from pydantic import BaseModel
class ItemUpdate(BaseModel):
name: str | None = None
price: float | None = None
description: str | None = None
@app.patch("/items/{item_id}", response_model=Item)
async def update_item(item_id: int, changes: ItemUpdate) -> Item:
"""
只更新傳入的欄位,其餘保持不變。
"""
stored = FAKE_DB.get(item_id)
if not stored:
raise HTTPException(status_code=404, detail="Item not found")
update_data = changes.dict(exclude_unset=True) # 只取有值的欄位
stored.update(update_data)
return Item(**stored)
exclude_unset=True能過濾掉 未提供 的欄位,避免把None寫回資料庫。
3.5 DELETE:刪除資源
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int):
"""
刪除指定商品,成功時回傳 204 No Content。
"""
if item_id not in FAKE_DB:
raise HTTPException(status_code=404, detail="Item not found")
del FAKE_DB[item_id]
return None # 204 不需要回傳內容
204 No Content為 刪除成功 的慣用回應碼。
3.6 OPTIONS:CORS 預檢與自訂回應
FastAPI 內建 CORS 中介軟體,但如果想手動回傳支援的方法,可這樣寫:
from fastapi.responses import JSONResponse
@app.options("/items")
async def options_items():
"""
回傳此路由支援的 HTTP 方法清單,供瀏覽器 CORS 預檢使用。
"""
return JSONResponse(
content={"methods": ["GET", "POST", "OPTIONS"]},
headers={"Allow": "GET, POST, OPTIONS"},
)
OPTIONS不會觸發路由的業務邏輯,只回傳 允許的 method 與必要的 CORS 標頭。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 使用可變的預設值(例如 dict、list)作為請求體 | 會在多個請求間共享同一個物件,導致資料被意外改寫。 | 永遠 使用 None 作為預設值,或使用 Field(default_factory=…)。 |
| 忽略狀態碼的語意 | 直接回傳 200 會讓前端無法分辨「建立」與「更新」的差異。 |
依照 REST 規範使用 201 Created、204 No Content、400 Bad Request…。 |
在 PATCH 中直接使用 item.dict() |
會把未提供的欄位寫成 None,造成資料遺失。 |
使用 exclude_unset=True 或 exclude_none=True。 |
未設定 response_model |
客戶端得不到自動產生的 OpenAPI 文件,且缺少資料驗證。 | 為每個路由明確指定 response_model,必要時使用 response_model_exclude_unset=True。 |
在路由函式內直接存取全域變數(例如 FAKE_DB) |
在多工作程式或容器環境下會產生競爭條件。 | 使用 依賴注入(Depends) 注入資料庫 session,或改用外部資料庫服務。 |
最佳實踐
- 型別安全:盡量在路由參數、請求體與回應模型上使用 Pydantic,讓 FastAPI 自動完成驗證與文件化。
- 非阻塞 I/O:所有可能涉及 I/O(資料庫、外部 API)皆使用
async/await,避免阻塞事件迴圈。 - 統一錯誤處理:使用
HTTPException或自訂的 exception handler,讓錯誤訊息保持一致。 - 分層設計:路由僅負責「接收與回傳」,業務邏輯放在 service 層,資料存取放在 repository 層,提升可測試性。
- 文件化:利用 FastAPI 自動產生的 Swagger UI(
/docs)與 ReDoc(/redoc),確保 API 文件永遠與程式碼同步。
實際應用場景
| 場景 | 需要的 HTTP 方法 | 為何選擇這些方法 |
|---|---|---|
| 電商商品管理後台 | GET(列表、細節) POST(新增商品) PUT(全域更新) PATCH(庫存、價格調整) DELETE(下架) |
完整的 CRUD 操作符合 REST 原則,前端可依需求呼叫對應方法。 |
| 使用者帳號設定 | GET(取得設定) PATCH(變更密碼、偏好) |
設定屬性多且常常只改變部份欄位,使用 PATCH 可減少資料傳輸。 |
| 即時聊天服務 | POST(發送訊息) GET(拉取訊息) OPTIONS(跨域檢查) |
訊息是不可變資源,使用 POST 建立,GET 只讀,OPTIONS 確保前端跨域順暢。 |
| 監控系統健康檢查 | GET(/ping) OPTIONS(檢查支援方法) |
簡單的 GET 用於負載平衡器的健康檢查,OPTIONS 可提供 CORS 預檢資訊。 |
小技巧:在大型專案中,建議將每個資源(如
items、users)的路由放在獨立的 router(APIRouter)模組,然後在主程式中統一掛載。這樣不僅結構清晰,也方便未來的微服務拆解。
總結
FastAPI 以 裝飾器 + 型別提示 的方式,讓我們可以在同一個檔案中快速定義 GET、POST、PUT、PATCH、DELETE、OPTIONS 六大 HTTP 方法的路由。藉由 Pydantic 的模型驗證、async 的非阻塞設計,以及自動產生的 OpenAPI 文件,開發者可以在保持程式簡潔的同時,確保 API 的正確性與可維護性。
- GET 用於查詢,POST 用於建立,PUT 用於完整取代,PATCH 用於部份更新,DELETE 用於刪除,OPTIONS 用於 CORS 預檢。
- 正確使用 狀態碼、response_model 與 依賴注入,能避免常見的陷阱並提升效能。
- 在實務上,將路由與業務邏輯分層、使用
APIRouter組織模組、搭配自動文件化,會讓大型專案的開發與維護變得更順暢。
掌握這些概念與技巧後,你就能以 FastAPI 建立符合 RESTful 標準、易於擴充且效能卓越的 Web API。祝開發順利!