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 序列化全都得自己手寫

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

items_db = {1: "Apple", 2: "Banana"}

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        # ① 自己解析路徑字串:拆出 "items" 和 id
        parts = self.path.strip("/").split("/")
        if len(parts) != 2 or parts[0] != "items":
            self._json(404, {"detail": "not found"})
            return

        # ② 自己驗證型別:字串轉 int,轉不過自己回 422
        try:
            item_id = int(parts[1])
        except ValueError:
            self._json(422, {"detail": "item_id 必須是整數"})
            return

        # ③ 自己查資料、自己決定狀態碼
        if item_id not in items_db:
            self._json(404, {"detail": f"item {item_id} not found"})
            return
        self._json(200, {"id": item_id, "name": items_db[item_id]})

    # ④ 自己組回應:設狀態碼、設 header、序列化 JSON、寫 bytes
    def _json(self, status, payload):
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(payload).encode())

HTTPServer(("127.0.0.1", 8000), Handler).serve_forever()

四個註解標出的,全是 每多一個 endpoint 就要再抄一遍的樣板 :解析路徑、驗證型別、決定狀態碼、序列化 JSON。再加上 GET 以外的方法要寫 do_POST 、body 要自己讀 self.rfilejson.loads ……商品稍微多幾種操作,這個檔案就會迅速膨脹、難以維護。而且:沒有自動文件、沒有 async。

其中 ① 那段 split("/") 比對路徑,做的就是 路由(routing / dispatch) ——決定「這個請求該交給哪段程式碼」,這正是所有 Web 框架的核心工作。它的來龍去脈,後面 為什麼需要 @app.get("/") 一節會完整拆解。

有 FastAPI:同一個 API#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from fastapi import FastAPI, HTTPException

app = FastAPI()
items_db = {1: "Apple", 2: "Banana"}

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id not in items_db:
        raise HTTPException(404, f"item {item_id} not found")
    return {"id": item_id, "name": items_db[item_id]}

上面那一大段樣板,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

1
pip install "fastapi[standard]"

[standard] 會一起裝 Uvicorn 和其他常用依賴(如 httpxjinja2),方便開發。生產環境若想最小安裝,可改成 pip install fastapi uvicorn


第一個 API#

建立 main.py

1
2
3
4
5
6
7
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello, FastAPI"}

啟動:

1
fastapi dev 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 程式攤開來看:

1
2
3
4
5
app = FastAPI()        # ① 建立應用實例(內部有一張空的路由表)

@app.get("/")          # ② 在路由表加一筆:method=GET, path=/, handler=root
async def root():
    return {"message": "Hello, 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 裝飾器語法糖,展開後等價於:

1
2
3
4
async def root():
    return {"message": "Hello, FastAPI"}

app.get("/")(root)        # 把 root 註冊到路由表

更底層一點,app.get("/") 其實是 app.add_api_route("/", ..., methods=["GET"]) 的包裝。FastAPI 內部維護的就是類似這樣的資料結構:

1
2
3
4
5
6
routes = [
    Route(path="/",        methods={"GET"},  endpoint=root),
    Route(path="/users",   methods={"GET"},  endpoint=list_users),
    Route(path="/users",   methods={"POST"}, endpoint=create_user),
    Route(path="/items/{item_id}", methods={"GET"}, endpoint=read_item),
]

每個請求進來,FastAPI 就走這個 list 比對 path 和 method ——找到了就執行對應 endpoint ,找不到就回 404。

對照其他框架#

這個「裝飾器 = 註冊路由」的模式不是 FastAPI 獨創,所有現代 Python Web 框架都是這套:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Flask
@app.route("/", methods=["GET"])
def root():
    return "Hello"

# FastAPI
@app.get("/")
async def root():
    return {"message": "Hello"}

# Django(風格不同,但本質一樣 —— 維護一張 urlpatterns 表)
urlpatterns = [
    path("", views.root),
]

Django 把路由表寫成顯式的 list;Flask 和 FastAPI 用裝飾器讓你「就地」把路由規則寫在函式旁邊。 底層概念完全一樣:URL + method → 函式 ——這就是 Web 應用的骨架。

理解到這裡,後面所有章節(路徑參數、查詢參數、Body、Depends …)都只是在回答同一個問題的細節:「FastAPI 從那段請求字串裡,到底要拆出哪些資訊餵給路徑函式?」


路徑參數(Path Parameters)#

URL 裡的變數部分用 {} 包起來,函式參數同名即可接收:

1
2
3
@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}
  • 訪問 /items/42read_item(item_id=42){"item_id": 42}
  • 訪問 /items/abc422 錯誤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/truetrue / false / 1 / 0
Enum 子類/mode/dark限定值集合(見下)

用 Enum 限制路徑參數的可選值#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from enum import Enum

class Color(str, Enum):
    red = "red"
    green = "green"
    blue = "blue"

@app.get("/colors/{color}")
async def pick_color(color: Color):
    return {"color": color.value}
  • 訪問 /colors/redpick_color(color=Color.red){"color": "red"}
  • 訪問 /colors/purple422 錯誤purple 不在 Color 列舉值內,pick_color 還沒被呼叫就被擋下)

查詢參數(Query Parameters)#

URL ? 後面的 key=value 部分,函式參數沒對應到路徑參數就會被當成查詢參數

1
2
3
@app.get("/search")
async def search(q: str, limit: int = 10):
    return {"q": q, "limit": limit}
  • 訪問 /search?q=pythonsearch(q="python", limit=10){"q": "python", "limit": 10}limit 沒給,套用預設值 10)
  • 訪問 /search?q=python&limit=5search(q="python", limit=5){"q": "python", "limit": 5}
  • 訪問 /search422 錯誤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 例外

獨立用起來大概是這樣:

1
2
3
4
5
6
7
8
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float

Item(name="Apple", price=30)      # OK;price 自動從 int 轉成 float
Item(name="Apple", price="free")  # ValidationError:price 不是數字

FastAPI 把這個能力 直接接到 HTTP body —— 你寫一個 BaseModel 類別,FastAPI 用它驗證進來的 JSON、把通過驗證的資料綁進函式參數。下面就用 POST /items 看一次。

在 FastAPI 裡用 Pydantic#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float
    is_offer: bool = False

@app.post("/items")
async def create_item(item: Item):
    return {"name": item.name, "total": item.price * 1.05}
① 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 會自動:

  1. 解析 JSON
  2. 驗證型別price 不是數字就 422)
  3. 轉成 Pydantic 物件 ,函式裡可以用 item.name 存取

進階驗證:Field#

要設更細的條件(最小值、字串長度、正則)用 Field

1
2
3
4
5
6
from pydantic import BaseModel, Field

class Item(BaseModel):
    name: str = Field(min_length=1, max_length=50)
    price: float = Field(gt=0, description="價格必須大於 0")
    tags: list[str] = Field(default_factory=list)

gtgeltle 分別是 > / ≥ / < / ≤。


回應模型(Response Model)#

response_model 參數可以限定 回傳給客戶端的欄位 ——常用來過濾掉密碼、內部欄位:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class UserIn(BaseModel):
    username: str
    password: str       # 收進來用
    email: str

class UserOut(BaseModel):
    username: str
    email: str          # 不含 password

@app.post("/users", response_model=UserOut)
async def create_user(user: UserIn):
    save_to_db(user)
    return user         # 即使回傳 UserIn,password 也會被過濾掉
① 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:

1
2
3
4
5
from fastapi import status

@app.post("/items", status_code=status.HTTP_201_CREATED)
async def create_item(item: Item):
    return item

fastapi.status 模組把所有 HTTP 狀態碼包成常數(如 HTTP_404_NOT_FOUND),比直接寫數字 404 易讀也不會手滑打錯。


例外處理:HTTPException#

業務邏輯要回 4xx / 5xx 錯誤時,用 HTTPException

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from fastapi import HTTPException

items_db = {1: "Apple", 2: "Banana"}

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id not in items_db:
        raise HTTPException(
            status_code=404,
            detail=f"item {item_id} not found",
        )
    return {"id": item_id, "name": items_db[item_id]}

detail 內容會以 JSON 形式回給客戶端:

1
{"detail": "item 3 not found"}

Pydantic 驗證失敗會自動回 422 ,不需要手動 raise;HTTPException 是給「資料格式正確、但業務邏輯不允許」的情況(如「找不到」、「沒權限」)。


依賴注入(Depends)#

Depends 是 FastAPI 最強的功能之一——把「每個路徑函式都會重複跑的邏輯」(讀 DB 連線、檢查 token、解析共用參數)抽成可重用的依賴函式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from fastapi import Depends

async def get_db():
    db = await connect_to_db()
    try:
        yield db
    finally:
        await db.close()

@app.get("/users/{user_id}")
async def read_user(user_id: int, db = Depends(get_db)):
    return await db.fetch_user(user_id)
  • Depends(get_db) 表示「進來這個路徑前,先跑 get_db ,把結果塞給 db
  • get_dbyield —— yield 前的程式碼在路徑函式 執行前 跑,yield 後在 回應之後 跑(適合 cleanup,類似 CH14 file IO 的 with
  • 依賴可以巢狀——get_db 自己又能 Depends 別的函式

共用查詢參數#

把分頁等共用參數抽成依賴:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
async def pagination(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

@app.get("/items")
async def list_items(page: dict = Depends(pagination)):
    return page

@app.get("/users")
async def list_users(page: dict = Depends(pagination)):
    return page

兩個路徑都自動接收 ?skip=0&limit=10 參數。


async def vs def#

FastAPI 路徑函式可以是 async def 也可以是普通 def ——選擇規則:

路徑函式用內部呼叫建議
async defawait some_async_lib()✅ 推薦,事件迴圈直接跑
defrequests.get() 等同步阻塞✅ FastAPI 自動放到 thread pool,不阻塞事件迴圈
async defrequests.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 和裝飾器參數 自動產生——你只要把程式寫好,文件自動就有了:

1
2
3
4
5
6
@app.post("/items", summary="建立商品", tags=["商品"])
async def create_item(item: Item):
    """
    建立一筆新商品,回傳含 ID 的完整紀錄。
    """
    return item

summary 顯示在 endpoint 列表的標題、tags 用來分組、docstring 顯示在展開後的描述區。


完整範例:簡易商品 API#

把上面所有要素串起來——一個有 CRUD 的簡易商品 API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field

app = FastAPI(title="商品 API")

class Item(BaseModel):
    name: str = Field(min_length=1, max_length=50)
    price: float = Field(gt=0)

class ItemOut(Item):
    id: int

items_db: dict[int, Item] = {}
next_id = 1

@app.get("/items", response_model=list[ItemOut])
async def list_items():
    return [ItemOut(id=i, **item.model_dump()) for i, item in items_db.items()]

@app.get("/items/{item_id}", response_model=ItemOut)
async def read_item(item_id: int):
    if item_id not in items_db:
        raise HTTPException(404, f"item {item_id} not found")
    return ItemOut(id=item_id, **items_db[item_id].model_dump())

@app.post("/items", response_model=ItemOut, status_code=status.HTTP_201_CREATED)
async def create_item(item: Item):
    global next_id
    items_db[next_id] = item
    result = ItemOut(id=next_id, **item.model_dump())
    next_id += 1
    return result

@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int):
    if item_id not in items_db:
        raise HTTPException(404, f"item {item_id} not found")
    del items_db[item_id]

打開 /docs 直接互動測試——不用裝 Postman、不用寫 curl。


常見錯誤#

async def 裡呼叫同步阻塞函式#

1
2
3
4
5
6
import requests

@app.get("/proxy")
async def proxy():
    r = requests.get("https://example.com")   # 卡住整個事件迴圈
    return r.text

要改成 httpx.AsyncClient

1
2
3
4
5
6
7
import httpx

@app.get("/proxy")
async def proxy():
    async with httpx.AsyncClient() as client:
        r = await client.get("https://example.com")
    return r.text

或乾脆把路徑函式改成普通 def , FastAPI 會自動丟到 thread pool。

dict 當回應而漏了 response_model#

1
2
3
@app.get("/users/{id}")
async def get_user(id: int):
    return {"id": id, "password": "secret"}   # 密碼直接外洩

response_model=UserOut 過濾敏感欄位。

把 Pydantic 模型當資料庫用#

Pydantic 模型只是 資料形狀的描述不會把資料存進資料庫 (沒有 .save() 之類方法)。要做真實的資料存取,要搭配 SQLAlchemySQLModel (FastAPI 作者同款)或 Tortoise ORM ——這類工具叫 ORM(Object-Relational Mapping) ,負責把資料表對應到 Python 類別、自動產生 SQL。

忘記 await 非同步函式#

1
2
3
@app.get("/users/{id}")
async def get_user(id: int, db = Depends(get_db)):
    return db.fetch_user(id)        # 漏掉 await,回傳協程物件而非結果

正確:

1
    return await db.fetch_user(id)

這類錯誤的成因見 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)
/docsSwagger UI 互動文件
/redocReDoc 靜態文件

小結#

FastAPI 的核心哲學是 「型別註解就是 API 規格」 ——你寫好 Python 型別、Pydantic 模型,框架就同時做完三件事:請求驗證、回應序列化、文件生成。少寫的程式碼換成可靠性,這也是它從 2018 年問世以來迅速取代 Flask 成為 Python API 主流的原因。

三句話記住這一章:

  1. 路徑參數寫在 URL {} 裡、查詢參數放函式簽名、Body 用 Pydantic 模型
  2. async def 配 async 函式庫;普通 def 配同步函式庫;不要混搭
  3. 不用另外寫文件—— /docs 永遠和程式碼同步

下一步可以延伸:

  • 資料庫整合 ——配合 SQLModel 或 SQLAlchemy 做真實 CRUD
  • 認證 ——用 OAuth2 + JWT 保護 endpoint(官方教學
  • 背景任務 —— BackgroundTasks 在回應後執行非同步工作
  • 部署 ——用 Uvicorn / Gunicorn + Docker 上 production