在第一篇教學中,我們建立了一個簡單的 / 根目錄端點。但真實世界的 API 通常需要根據不同的請求回傳不同的資料,例如:取得「特定 ID」的使用者資料。
這時候,我們就需要用到路徑參數(Path Parameters)。
內容大綱
1. 前置知識:什麼是「動態 URL」?
靜態 URL 是固定的網址,例如 /users/list,每次造訪都會看到一樣的東西。
而動態 URL 則允許網址的某個部分變成「變數」。例如購物網站的商品網址可能是 /products/1、/products/2,其中 1 和 2 就是動態改變的部分。後端程式會讀取這個數字,然後從資料庫撈出對應的商品資料。
2. 什麼是路徑參數?
在 URL 中,那個會動態改變的部分,我們就稱為路徑參數。
在 FastAPI 裡,我們可以使用類似 Python f-string 的語法,用大括號 {} 將 URL 中的某個部分包起來,宣告它是一個變數。
3. 宣告路徑參數
讓我們在 main.py 中新增一個端點,用來取得特定使用者的資料:
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
def read_user(user_id):
return {"user_id": user_id}
在這個例子中:
/users/{user_id}告訴 FastAPI:「/users/後面的那一截字串,請當作變數user_id」。- 路由函式
def read_user(user_id):必須接收同名的參數user_id。
如果你在瀏覽器輸入 http://127.0.0.1:8000/users/alice,就會看到回傳:
{"user_id": "alice"}
⚠️ 注意:不要宣告重複的路徑
如果你寫了兩個一模一樣的路由,例如:
@app.get("/test") def test_1(): return {"Test": "1!"} @app.get("/test") def test_2(): return {"Test": "2!"}FastAPI 的比對規則是 「從上往下掃描,先搶先贏」。當有人造訪
/test時,任務會立刻交給第一個test_1處理並結束。 下面的test_2函式會變成永遠等不到客人的「幽靈程式碼」。此外,這還會導致自動產生的 API 文件(Swagger UI)發生覆蓋錯亂。
4. 加上型別提示:自動型別轉換
在上面的例子中,user_id 預設會被當作字串(string)。但如果使用者的 ID 在資料庫中是整數(Integer)怎麼辦?
在 FastAPI 中,只需要使用 Python 標準的型別提示(Type Hints),FastAPI 就會自動幫忙做轉換。
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id, "type": type(item_id).__name__}
當你造訪 http://127.0.0.1:8000/items/5,FastAPI 發現你宣告了 item_id: int,就會自動把網址中的字串 "5" 轉換成整數 5 傳進函式中。
5. 自動資料驗證(422 錯誤)
延續上面的例子,如果有人亂輸入網址,造訪了 http://127.0.0.1:8000/items/foo,會發生什麼?字串 "foo" 沒辦法變成整數啊。
程式不會崩潰,FastAPI 會直接攔截這個錯誤,並回傳清楚的 HTTP 錯誤狀態碼 422 Unprocessable Entity(無法處理的實體),並附帶錯誤說明:
{
"detail": [
{
"type": "int_parsing",
"loc": [
"path",
"item_id"
],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo"
}
]
}
錯誤訊息明確指出:在路徑(path)中的 item_id 發生了轉換整數失敗的錯誤。這省去了我們自己寫 if not item_id.isdigit(): ... 等檢查邏輯。
6. 路徑順序的重要性
在設計 API 時,有時候會同時有「固定路徑」和「動態路徑」。
例如,我們有一個取得所有使用者的端點 /users/all,也有一個取得特定使用者的端點 /users/{user_id}。
在 FastAPI 中,路由的宣告順序非常重要。程式碼是從上往下執行的,FastAPI 會比對第一個符合條件的路徑。
錯誤的寫法:
@app.get("/users/{user_id}")
def read_user(user_id: str):
return {"user_id": user_id}
@app.get("/users/all")
def read_all_users():
return ["Alice", "Bob"]
如果這樣寫,當你造訪 /users/all 時,FastAPI 會先看到 @app.get("/users/{user_id}"),然後把 "all" 當作是變數 user_id 的值傳進去,這是錯誤的。
正確的寫法(固定路徑在前):
@app.get("/users/all")
def read_all_users():
return ["Alice", "Bob"]
@app.get("/users/{user_id}")
def read_user(user_id: str):
return {"user_id": user_id}
把固定的路由寫在前面,就能確保它會被正確攔截。
7. 使用 Enum 限制合法選項
有時候,路徑參數不能隨便亂填,只能是幾個特定的選項。這時候可以使用 Python 內建的 Enum。
假設我們有一個機器學習模型 API,只接受 alexnet、resnet、lenet 三種模型名稱:
from enum import Enum
from fastapi import FastAPI
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
app = FastAPI()
@app.get("/models/{model_name}")
def get_model(model_name: ModelName):
if model_name == ModelName.alexnet:
return {"model_name": model_name, "message": "Deep Learning FTW!"}
if model_name.value == "lenet":
return {"model_name": model_name, "message": "LeCNN all the masses"}
return {"model_name": model_name, "message": "Have some residuals"}
這個寫法有三個好處:
- 編輯器支援:打字時會有自動補全(Auto-completion)。
- 自動驗證:如果使用者輸入
http://127.0.0.1:8000/models/yolov4,FastAPI 會直接回傳 422 錯誤,並告訴使用者有哪些合法的選項。
{
"detail": [
{
"type": "enum",
"loc": [
"path",
"model_name"
],
"msg": "Input should be 'alexnet', 'resnet' or 'lenet'",
"input": "yolov4",
"ctx": {
"expected": "'alexnet', 'resnet' or 'lenet'"
}
}
]
}
- API 文件:Swagger UI 會自動把這個欄位變成一個下拉式選單,讓測試更方便!
8. 包含路徑分隔符的路徑參數
最後一個比較特殊的情境是:如果路徑參數本身就是一個檔案路徑(包含 /)怎麼辦?
例如 URL 長這樣:/files/home/johndoe/myfile.txt。
我們希望 /files/ 後面的所有東西 home/johndoe/myfile.txt 都被當成一個變數。
FastAPI 支援 Starlette 的特殊路徑語法::path。
@app.get("/files/{file_path:path}")
def read_file(file_path: str):
return {"file_path": file_path}
只要加上 :path,FastAPI 就不會因為遇到 / 就把字串切斷,而是會把它一字不漏地全部抓下來。
測試範例:
如果在瀏覽器輸入 http://127.0.0.1:8000/files/home/myfile.txt,將會收到完整的路徑字串:
{"file_path": "home/myfile.txt"}
練習題
📝 練習題 1:撰寫產品分類 API
題目:撰寫一個 API 端點 GET /categories/{category_name}。
這個端點應該接收字串型別的 category_name,並回傳 JSON 格式的分類名稱。
答案:
from fastapi import FastAPI
app = FastAPI()
@app.get("/categories/{category_name}")
def get_category(category_name: str):
return {"category": category_name}
📝 練習題 2:修復順序錯誤
題目:以下的程式碼有個問題。如果造訪 /posts/latest 會發生什麼事?請將程式碼修改成正確的順序。
from fastapi import FastAPI
app = FastAPI()
@app.get("/posts/{post_id}")
def get_post(post_id: int):
return {"post_id": post_id}
@app.get("/posts/latest")
def get_latest_post():
return {"post": "This is the latest post!"}
答案:
如果照著原本的寫法,造訪 /posts/latest 時,FastAPI 會以為 "latest" 是 post_id。由於 post_id 規定要是整數(int),所以會丟出 422 Validation Error 錯誤。
正確寫法(把固定路徑移到上面):
from fastapi import FastAPI
app = FastAPI()
@app.get("/posts/latest")
def get_latest_post():
return {"post": "This is the latest post!"}
@app.get("/posts/{post_id}")
def get_post(post_id: int):
return {"post_id": post_id}
📝 練習題 3:使用 Enum 限制顏色
題目:建立一個 GET /items/color/{color_name} 的端點。
請使用 Enum 限制 color_name 只能是 red、green 或 blue 其中一種。如果輸入合法,回傳對應的顏色名稱。
答案:
from enum import Enum
from fastapi import FastAPI
app = FastAPI()
class ColorName(str, Enum):
red = "red"
green = "green"
blue = "blue"
@app.get("/items/color/{color_name}")
def get_color(color_name: ColorName):
return {"selected_color": color_name}