FastAPI#
FastAPI 是 Python 用來寫 Web API 的現代框架,2018 年問世,現在已是 Python 後端的主流選擇。三個賣點:
- 快 ——底層用 Starlette + Uvicorn,效能接近 Node.js 和 Go
- 型別驅動 ——靠 Python 型別註解(type hints)和 Pydantic 自動做請求資料驗證
- 自動文件 ——啟動後直接生出可互動的 Swagger UI / ReDoc
這一章接在 CH17 async / await 之後——FastAPI 路徑函式天生支援 async def,正是上一章 async 在 Web 後端最常見的實戰場景。
有 FastAPI 和沒有 FastAPI 的差別#
要理解 FastAPI 的價值,最快的方式是把 同一個 API 寫兩次 ——一次只用 Python 標準函式庫、一次用 FastAPI ——直接看差距。
任務很單純:做一個 GET /items/{item_id} ,回傳對應商品;item_id 必須是整數,找不到就回 404。
沒有 FastAPI:用標準函式庫 http.server#
Python 內建的 http.server 可以不裝任何套件就跑一個 HTTP 伺服器,但 路由、型別驗證、狀態碼、JSON 序列化全都得自己手寫 :
| |
四個註解標出的,全是 每多一個 endpoint 就要再抄一遍的樣板 :解析路徑、驗證型別、決定狀態碼、序列化 JSON。再加上 GET 以外的方法要寫 do_POST 、body 要自己讀 self.rfile 再 json.loads ……商品稍微多幾種操作,這個檔案就會迅速膨脹、難以維護。而且:沒有自動文件、沒有 async。
其中 ① 那段 split("/") 比對路徑,做的就是 路由(routing / dispatch) ——決定「這個請求該交給哪段程式碼」,這正是所有 Web 框架的核心工作。它的來龍去脈,後面 為什麼需要 @app.get("/") 一節會完整拆解。
有 FastAPI:同一個 API#
| |
上面那一大段樣板,FastAPI 全部幫你做掉了:
| 工作 | 沒有 FastAPI | 有 FastAPI |
|---|---|---|
| 路由分派 | 自己 split("/") 比對 | @app.get("/items/{item_id}") |
| 型別驗證 | 自己 try/except int() | 註解 item_id: int ,錯了自動回 422 |
| 狀態碼 | 自己 send_response(404) | raise HTTPException(404, ...) |
| JSON 序列化 | 自己 json.dumps().encode() | 回傳 dict 自動轉 |
| 互動文件 | 沒有 | 自動產生 /docs |
程式碼從 30 幾行縮成 10 行,少寫的全是樣板、不是邏輯 。這就是 FastAPI 的核心價值——把 HTTP 的繁瑣細節收進框架,讓你只專心寫「這個請求要回什麼」。
為什麼選 FastAPI#
既然框架能把這些樣板收掉,那 Python 寫 Web API 該選哪個框架?主要有三個選擇:
| 框架 | 風格 | 適合場景 |
|---|---|---|
| Flask | 老牌微框架,同步為主 | 小型專案、教學、有大量 Flask 套件生態 |
| Django | 全功能框架(ORM、admin、auth) | 內容網站、需要後台管理介面 |
| FastAPI | 現代 async 框架,型別驅動 | 高併發 API、微服務、即時應用 |
上面那 10 行展示的原生 async(見 CH17 async / await)、自動驗證、自動文件,正是 FastAPI 相較 Flask 多出來的三件事。後面每一節,都是在拆解那段範例裡 FastAPI 到底替你做了哪些事。
安裝#
FastAPI 本身只是框架,要配合 ASGI server 才能跑起來。官方推薦 Uvicorn:
| |
[standard] 會一起裝 Uvicorn 和其他常用依賴(如 httpx、jinja2),方便開發。生產環境若想最小安裝,可改成 pip install fastapi uvicorn。
第一個 API#
建立 main.py:
| |
啟動:
| |
打開瀏覽器:
http://127.0.0.1:8000/→ 看到{"message": "Hello, FastAPI"}http://127.0.0.1:8000/docs→ 自動生成的 Swagger UI ,可以直接點按鈕送請求http://127.0.0.1:8000/redoc→ 另一套自動文件 ReDoc
fastapi dev 是 0.110+ 加入的開發指令,會自動 reload;舊版可用 uvicorn main:app --reload。
短短幾行就跑起一個 Web API ——函式回傳的 dict 會被 FastAPI 自動序列化成 JSON。但這個 @app.get("/") 裝飾器到底在做什麼?下一節從一個 HTTP 請求的本質開始說明。
為什麼需要 @app.get("/") :路由與 Web 架構#
@app.get("/") 看起來只是個裝飾器,但它正是整個 Web 應用的骨架。要理解它在做什麼,得先看一個 HTTP 請求的本質。
一個 HTTP 請求到底是什麼#
當你在瀏覽器打 http://127.0.0.1:8000/ 按 Enter,瀏覽器送出去的不是「我要那個網頁」,而是一段純文字訊息:
GET / HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Mozilla/5.0 ...第一行是關鍵:
GET—— HTTP 方法(method) ,告訴伺服器「我想做什麼」(GET 讀取、POST 建立、DELETE 刪除…)/—— 路徑(path) ,告訴伺服器「我要哪個資源」
伺服器收到後要回一段同樣是純文字的回應:
HTTP/1.1 200 OK
Content-Type: application/json
{"message": "Hello, FastAPI"}整個 Web 就是這樣「送 request 字串、收 response 字串」的對話 。瀏覽器只是把這層細節包起來給你看而已。
Web 應用的核心問題:dispatch(分派)#
一個網站通常有幾十、幾百個不同的 URL:
GET / → 首頁
GET /users → 使用者列表
GET /users/42 → 編號 42 的使用者
POST /users → 建立新使用者
GET /items/5 → 編號 5 的商品
DELETE /items/5 → 刪除編號 5 的商品伺服器程式收到一個請求字串時,最根本的問題是:「這個請求要交給哪段程式碼處理?」 這個查表動作叫 路由(routing) 或 dispatch 。
@app.get("/") 做的就是 註冊一條路由規則 :
「以後只要看到
GET /,就交給root這個函式處理。」
把整段 FastAPI 程式攤開來看:
| |
@app.get("/") 不是「定義 endpoint」的神奇語法,它就是 一行 Python 程式碼,把 root 函式登記到 app 的路由表裡 。沒有它,FastAPI 不知道 root 函式存在,請求進來找不到對應就回 404。
整體請求流程#
把所有層次串起來看一次:
sequenceDiagram
participant Browser as 瀏覽器
participant Server as Uvicorn<br/>(ASGI server)
participant App as FastAPI app
Note over Browser: ① 打字 / 點連結<br/>組出 HTTP 請求字串<br/>"GET / HTTP/1.1..."
Browser->>Server: HTTP 請求
Note over Server: ② 解析請求字串<br/>解成 dict:<br/>{method: "GET", path: "/", headers: {...}}
Server->>App: 結構化請求 (ASGI scope)
Note over App: ③ 查路由表<br/>GET / → root
Note over App: ④ 呼叫 root()<br/>拿到 {"message": ...}
Note over App: ⑤ 序列化成 JSON
App-->>Server: 結構化回應
Note over Server: ⑥ 包成 HTTP 回應字串<br/>"HTTP/1.1 200 OK\nContent-Type: ..."
Server-->>Browser: HTTP 回應
Note over Browser: ⑦ 解析回應、顯示內容各層的職責切得很乾淨:
| 元件 | 做的事 |
|---|---|
| 瀏覽器 | 把使用者動作翻譯成 HTTP 請求字串、解析回應字串顯示出來 |
| Uvicorn(ASGI server) | 監聽 TCP 連線、把請求字串解析成結構化資料、把結構化回應包回字串 |
| FastAPI app | 拿著結構化請求 查路由表 、執行對應函式、把結果丟回去 |
路徑函式 (root) | 真正寫業務邏輯的地方 |
@app.get("/") 就是 第 ③ 步那張路由表的一行 ——沒有它,FastAPI 在第 ③ 步查不到對應,會回 404 Not Found ;有 path 但 method 不對(比方說對 / 送 POST),會回 405 Method Not Allowed 。
拆開裝飾器看本質#
@app.get("/") 是 Python 裝飾器語法糖,展開後等價於:
| |
更底層一點,app.get("/") 其實是 app.add_api_route("/", ..., methods=["GET"]) 的包裝。FastAPI 內部維護的就是類似這樣的資料結構:
| |
每個請求進來,FastAPI 就走這個 list 比對 path 和 method ——找到了就執行對應 endpoint ,找不到就回 404。
對照其他框架#
這個「裝飾器 = 註冊路由」的模式不是 FastAPI 獨創,所有現代 Python Web 框架都是這套:
| |
Django 把路由表寫成顯式的 list;Flask 和 FastAPI 用裝飾器讓你「就地」把路由規則寫在函式旁邊。 底層概念完全一樣:URL + method → 函式 ——這就是 Web 應用的骨架。
理解到這裡,後面所有章節(路徑參數、查詢參數、Body、Depends …)都只是在回答同一個問題的細節:「FastAPI 從那段請求字串裡,到底要拆出哪些資訊餵給路徑函式?」
路徑參數(Path Parameters)#
URL 裡的變數部分用 {} 包起來,函式參數同名即可接收:
| |
- 訪問
/items/42→read_item(item_id=42)→{"item_id": 42} - 訪問
/items/abc→ 422 錯誤 (abc不是 int,read_item還沒被呼叫就被擋下)
注意 item_id: int 這個型別註解—— FastAPI 會自動把 URL 上的字串轉成 int,轉不過就回 422。型別註解不只是給人看的提示,是實際的驗證與轉換規則 。
路徑參數的型別#
FastAPI 支援這些常見型別:
| 型別 | URL 範例 | 說明 |
|---|---|---|
int | /items/42 | 整數 |
float | /price/9.99 | 浮點數 |
str | /users/alice | 字串(預設) |
bool | /flag/true | true / false / 1 / 0 |
Enum 子類 | /mode/dark | 限定值集合(見下) |
用 Enum 限制路徑參數的可選值#
| |
- 訪問
/colors/red→pick_color(color=Color.red)→{"color": "red"} - 訪問
/colors/purple→ 422 錯誤 (purple不在Color列舉值內,pick_color還沒被呼叫就被擋下)
查詢參數(Query Parameters)#
URL ? 後面的 key=value 部分,函式參數沒對應到路徑參數就會被當成查詢參數 :
| |
- 訪問
/search?q=python→search(q="python", limit=10)→{"q": "python", "limit": 10}(limit沒給,套用預設值 10) - 訪問
/search?q=python&limit=5→search(q="python", limit=5)→{"q": "python", "limit": 5} - 訪問
/search→ 422 錯誤 (q沒給又沒有預設值, 必填 ,search還沒被呼叫就被擋下)
必填 vs 選填#
| 寫法 | 必填? | 行為 |
|---|---|---|
q: str | 必填 | 沒給就 422 |
q: str = "default" | 選填 | 沒給套用預設值 |
q: str | None = None | 選填 | 沒給就是 None |
str \| None 是 Python 3.10+ 的型別語法,舊版要用 Optional[str] (from typing import Optional)。
請求主體(Request Body)#
POST / PUT 通常要帶 JSON 資料。FastAPI 用 Pydantic 模型來描述資料結構, 型別註解就是驗證規則 。
先認識 Pydantic#
Pydantic 是一個 獨立的 Python 函式庫 (不是 FastAPI 內建,但 FastAPI 把它當核心依賴),核心能力是:
- 用 「繼承
BaseModel的類別 + 型別註解」 描述資料形狀 - 給它一個 dict,它會 按欄位型別驗證、必要時自動轉型 ,產出物件
- 對不上規則就丟
ValidationError例外
獨立用起來大概是這樣:
| |
FastAPI 把這個能力 直接接到 HTTP body —— 你寫一個 BaseModel 類別,FastAPI 用它驗證進來的 JSON、把通過驗證的資料綁進函式參數。下面就用 POST /items 看一次。
在 FastAPI 裡用 Pydantic#
| |
① HTTP 請求
POST /items
body:{name:'Apple', price:30, is_offer:true}
│
│ 解析 JSON + Pydantic 驗證
│
├──▶ 驗證失敗(例:price='free')──▶ 422 錯誤
│ (create_item 不會被呼叫)
│
▼ 驗證通過
② 函式呼叫
create_item(item=Item(name='Apple', price=30, is_offer=True))
※ body 沒給 is_offer 時,套用 Pydantic 模型預設值 False
│
│ 執行 handler
▼
③ JSON 回應
{name:'Apple', total:31.5}FastAPI 會自動:
- 解析 JSON
- 驗證型別 (
price不是數字就 422) - 轉成 Pydantic 物件 ,函式裡可以用
item.name存取
進階驗證:Field#
要設更細的條件(最小值、字串長度、正則)用 Field :
| |
gt、ge、lt、le 分別是 > / ≥ / < / ≤。
回應模型(Response Model)#
response_model 參數可以限定 回傳給客戶端的欄位 ——常用來過濾掉密碼、內部欄位:
| |
① HTTP 請求
POST /users
body:{username:'alice', password:'secret', email:'a@x.com'}
│
│ 解析 + 驗證
▼
② 函式呼叫
create_user(user=UserIn(
username='alice', password='secret', email='a@x.com'))
│
│ 執行 handler
▼
③ 函式回傳
return user(含 password)
│
│ response_model=UserOut 過濾
▼
④ JSON 回應
{username:'alice', email:'a@x.com'}
(password 不見了)比起前面幾個小節,這裡多了一個階段—— 函式回傳之後 還會經過 response_model 過濾才變成 JSON 回應(圖中 ③ → ④ 那一段)。前面的「驗證」發生在 handler 之前(① → ②),這裡的「過濾」發生在 handler 之後(③ → ④)。
UserIn / UserOut 是常見的命名慣例——「進來」和「出去」的資料形狀通常不同。
HTTP 方法與狀態碼#
FastAPI 把常見 HTTP 方法都包成裝飾器:
| 裝飾器 | 用途 |
|---|---|
@app.get | 讀取資源 |
@app.post | 建立資源 |
@app.put | 完整更新資源 |
@app.patch | 部分更新資源 |
@app.delete | 刪除資源 |
自訂狀態碼#
預設成功回 200,建立資源時應該回 201:
| |
fastapi.status 模組把所有 HTTP 狀態碼包成常數(如 HTTP_404_NOT_FOUND),比直接寫數字 404 易讀也不會手滑打錯。
例外處理:HTTPException#
業務邏輯要回 4xx / 5xx 錯誤時,用 HTTPException :
| |
detail 內容會以 JSON 形式回給客戶端:
| |
Pydantic 驗證失敗會自動回 422 ,不需要手動 raise;HTTPException 是給「資料格式正確、但業務邏輯不允許」的情況(如「找不到」、「沒權限」)。
依賴注入(Depends)#
Depends 是 FastAPI 最強的功能之一——把「每個路徑函式都會重複跑的邏輯」(讀 DB 連線、檢查 token、解析共用參數)抽成可重用的依賴函式:
| |
Depends(get_db)表示「進來這個路徑前,先跑get_db,把結果塞給db」get_db用yield——yield前的程式碼在路徑函式 執行前 跑,yield後在 回應之後 跑(適合 cleanup,類似 CH14 file IO 的 with)- 依賴可以巢狀——
get_db自己又能Depends別的函式
共用查詢參數#
把分頁等共用參數抽成依賴:
| |
兩個路徑都自動接收 ?skip=0&limit=10 參數。
async def vs def#
FastAPI 路徑函式可以是 async def 也可以是普通 def ——選擇規則:
| 路徑函式用 | 內部呼叫 | 建議 |
|---|---|---|
async def | await some_async_lib() | ✅ 推薦,事件迴圈直接跑 |
def | requests.get() 等同步阻塞 | ✅ FastAPI 自動放到 thread pool,不阻塞事件迴圈 |
async def | requests.get() 同步阻塞 | ❌ 卡住整個事件迴圈,最糟糕的組合 |
簡單規則:用了 async 函式庫就 async def、用同步函式庫就 def 。混搭最危險。
詳細的 async 行為見 CH17 async / await。
自動文件:Swagger UI 與 ReDoc#
啟動後直接訪問:
/docs—— Swagger UI ,可以直接展開每個 endpoint、填參數、按 Execute 送請求看結果/redoc—— ReDoc ,閱讀導向、適合給 API 使用者看的文件/openapi.json—— 原始 OpenAPI schema,可以匯入 Postman / 程式碼產生器
文件內容是從 型別註解、Pydantic 模型、docstring 和裝飾器參數 自動產生——你只要把程式寫好,文件自動就有了:
| |
summary 顯示在 endpoint 列表的標題、tags 用來分組、docstring 顯示在展開後的描述區。
完整範例:簡易商品 API#
把上面所有要素串起來——一個有 CRUD 的簡易商品 API:
| |
打開 /docs 直接互動測試——不用裝 Postman、不用寫 curl。
常見錯誤#
在 async def 裡呼叫同步阻塞函式#
| |
要改成 httpx.AsyncClient :
| |
或乾脆把路徑函式改成普通 def , FastAPI 會自動丟到 thread pool。
用 dict 當回應而漏了 response_model#
| |
加 response_model=UserOut 過濾敏感欄位。
把 Pydantic 模型當資料庫用#
Pydantic 模型只是 資料形狀的描述 , 不會把資料存進資料庫 (沒有 .save() 之類方法)。要做真實的資料存取,要搭配 SQLAlchemy 、SQLModel (FastAPI 作者同款)或 Tortoise ORM ——這類工具叫 ORM(Object-Relational Mapping) ,負責把資料表對應到 Python 類別、自動產生 SQL。
忘記 await 非同步函式#
| |
正確:
| |
這類錯誤的成因見 CH17 「忘記 await 協程物件」。
速查表#
| 語法 | 作用 |
|---|---|
app = FastAPI() | 建立應用實例 |
@app.get("/path") | 註冊 GET 路徑(也有 post / put / patch / delete) |
item_id: int (路徑) | 路徑參數,自動驗證型別 |
q: str = "x" (函式) | 查詢參數,有預設值就是選填 |
item: BaseModel 子類 | 請求 body,FastAPI 自動解析 JSON 並驗證 |
response_model=Schema | 限定回應欄位,過濾敏感資料 |
status_code=201 | 自訂成功時的 HTTP 狀態碼 |
raise HTTPException(404, "...") | 主動回錯誤狀態 |
Depends(func) | 依賴注入,可重用前置邏輯 |
fastapi dev main.py | 啟動開發伺服器(自動 reload) |
/docs | Swagger UI 互動文件 |
/redoc | ReDoc 靜態文件 |
小結#
FastAPI 的核心哲學是 「型別註解就是 API 規格」 ——你寫好 Python 型別、Pydantic 模型,框架就同時做完三件事:請求驗證、回應序列化、文件生成。少寫的程式碼換成可靠性,這也是它從 2018 年問世以來迅速取代 Flask 成為 Python API 主流的原因。
三句話記住這一章:
- 路徑參數寫在 URL
{}裡、查詢參數放函式簽名、Body 用 Pydantic 模型 async def配 async 函式庫;普通def配同步函式庫;不要混搭- 不用另外寫文件——
/docs永遠和程式碼同步
下一步可以延伸: