Web Day 4 FastAPI 起步:路由、Pydantic v2 與自動文件
執行需求:CPU 可跑。經過前三天把地圖、環境、HTTP/REST 觀念都建立起來之後,今天要正式進入 FastAPI 的核心機制。你會學到 FastAPI 怎麼用裝飾器組合複雜的 URL 模式、Pydantic v2 怎麼幫你設計嚴謹的資料模型、為什麼 FastAPI 能自動產生 Swagger UI 與 ReDoc 兩份互動式 API 說明頁面,最後用一個「書本管理 API」範例把所有東西串起來。這個範例會是後續每天演進的基礎。
引言
如果把寫後端比喻成蓋房子,前三天做的是看地圖(系列導覽)、準備工具(環境與工具鏈)、理解建築法規(HTTP 與 REST)。今天開始要真正搬磚了——把第一面牆砌起來。FastAPI 之所以在 2018 年推出後迅速成為 Python 後端的主流框架,核心原因有三:基於 Python 型別提示(type hints)做自動驗證、效能接近 Node.js 與 Go、生態系(特別是 Pydantic v2)完整。它的設計哲學是「讓型別提示就是規格、讓規格就是測試」,所以工程師寫一次程式就能拿到正確的請求驗證、自動產生的 OpenAPI 規格、以及互動式 API 說明頁面。
這篇文章會帶你深入 FastAPI 的三個核心:路由(routing)把 URL 對應到 Python 函式;Pydantic v2 模型(schema)描述請求與回應的形狀;自動說明頁面(automatic documentation)讓 Swagger UI 與 ReDoc 直接從程式碼生出來。今天的範例會做一個簡單的「書本管理 API」,支援建立、讀取、更新、刪除書本資料,並用型別提示做嚴格的請求驗證。學完之後,你就具備寫一個完整 CRUD 後端的能力。
FastAPI 的核心概念
FastAPI 是站在兩個巨人肩上建起來的:Starlette(處理 HTTP 與 ASGI)與 Pydantic(處理資料驗證)。它的「最小核心」其實就是一個 FastAPI 物件加上若干路由函式:
# 最小的 FastAPI 程式
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
return {"message": "hello"}
# 啟動:uvicorn main:app --reload
FastAPI() 物件負責整個應用程式的設定、路由表、說明頁面生成;@app.get("/") 是裝飾器,把下面的函式註冊成「GET /」的處理器。FastAPI 用 Starlette 的路由系統,所以支援路徑參數、查詢參數、相依性注入、中介軟體(middleware)等所有標準功能。型別提示(例如 item_id: int)則會被 Pydantic 拿去做自動驗證。
FastAPI 的另一個強項是「說明頁面」。只要你在函式簽章用了型別提示,並用 Pydantic 模型描述請求與回應,FastAPI 就能從你的程式碼直接生出 OpenAPI 規格,然後渲染成兩種互動式說明頁面:Swagger UI(在 /docs)與 ReDoc(在 /redoc)。這對前後端協作是革命性的改進,前端工程師可以直接從說明頁面看到「每個端點要送什麼、回什麼」,不再需要翻後端程式碼或問後端工程師。
路由:路徑參數、查詢參數與請求主體
FastAPI 把 HTTP 請求的不同部分對應到 Python 函式的不同參數:
| HTTP 部位 | Python 參數 | 範例 |
|---|---|---|
| 路徑參數(path parameter) | 函式參數 | /items/{item_id} → item_id: int |
| 查詢參數(query string) | 有預設值的函式參數 | /items?skip=0&limit=10 → skip: int = 0 |
| 請求主體(body) | Pydantic 模型 | POST /items + JSON → payload: ItemCreate |
| 標頭(headers) | 由 Header() 注入 |
x-token: str = Header(...) |
| Cookies | 由 Cookie() 注入 |
session_id: str = Cookie(None) |
FastAPI 靠「參數是否有型別提示」與「是否有預設值」來判斷這個參數屬於哪一類。如果參數是簡單型別(int、str、bool、uuid.UUID)且沒有預設值,就是路徑參數;如果有預設值,就是查詢參數(可選);如果是 Pydantic 模型,則是請求主體。這個推導機制讓函式簽章非常乾淨,不需要像 Flask 那樣到處寫 request.args.get(...)。
路徑參數可以做嚴格的型別驗證:
# 路徑參數自動驗證
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
# 如果 item_id 不是整數(例如 /items/abc)會回 422
return {"item_id": item_id * 2}
@app.get("/files/{file_path:path}")
def read_file(file_path: str):
# 用 :path 修飾符可以讓斜線也包含在參數裡
return {"file_path": file_path}
@app.get("/users/{user_id}")
def read_user(user_id: int, q: str | None = None):
# user_id 是必填路徑參數;q 是選填查詢參數
return {"user_id": user_id, "q": q}
第一個端點示範了型別驗證:如果你打 /items/abc,FastAPI 會自動回 422 並告訴你「item_id 應該是整數」,不需要你寫任何檢查程式。第二個端點示範了 :path 修飾符——預設路徑參數不會包含斜線,如果你想讓整段路徑都當作一個參數,要明確寫 :path。第三個端點展示了選填查詢參數 q: str | None = None。
Pydantic v2 資料模型
Pydantic v2 是 FastAPI 的靈魂。它讓你用 Python class 定義「資料長什麼樣」,Pydantic 自動處理驗證、序列化、與 JSON 轉換。FastAPI 預設用 Pydantic v2(2.x 版),跟 v1 的 API 差很多,這系列從一開始就用 v2 的寫法:
# Pydantic v2 的資料模型寫法
from datetime import datetime
from pydantic import BaseModel, EmailStr, Field
class User(BaseModel):
# 必填欄位
id: int
username: str = Field(min_length=3, max_length=40)
email: EmailStr
# 選填欄位(有預設值)
is_active: bool = True
created_at: datetime | None = None
# 自動驗證
user = User(id=1, username="alice", email="alice@example.com")
print(user.username)
# 輸出:alice
# 不合法的資料會拋 ValidationError
try:
User(id=2, username="al", email="not-an-email")
except Exception as e:
print("驗證失敗:", type(e).__name__)
# 輸出:驗證失敗: ValidationError
Pydantic v2 的關鍵設計:欄位預設值是「型別提示的順序」,必填欄位放前面;用 Field(min_length=3) 等內建約束描述限制;用 EmailStr 等特殊型別做格式驗證;可以巢狀其他 Pydantic 模型;可以自訂驗證器(validator)。這套機制讓「資料形狀」和「程式邏輯」完全分離,改 API 規格只要改 Pydantic 模型,其他程式碼自動跟著更新。
API 設計通常會區分「請求模型」與「回應模型」。例如建立使用者時不帶 id(伺服器給)、更新時所有欄位都可選:
# 區分請求與回應模型
from pydantic import BaseModel, ConfigDict, Field
class UserBase(BaseModel):
username: str = Field(min_length=3, max_length=40)
email: str = Field(pattern=r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
class UserCreate(UserBase):
# 建立請求:帶密碼,不帶 id
password: str = Field(min_length=8, max_length=128)
class UserUpdate(BaseModel):
# 更新請求:所有欄位都可選
username: str | None = Field(default=None, min_length=3, max_length=40)
email: str | None = None
is_active: bool | None = None
class UserPublic(UserBase):
# 回應模型:帶 id 與時間戳,不帶密碼
id: int
is_active: bool = True
model_config = ConfigDict(from_attributes=True)
# from_attributes=True 讓 Pydantic 可以從 ORM 物件(Day 6)建立模型
這個寫法展現了 API 設計的兩個好習慣:第一,UserPublic 不包含密碼欄位,避免不小心把密碼雜湊洩漏到 API 回應;第二,UserUpdate 用 None 表達「不更新這個欄位」,讓 PATCH 端點可以只更新部分欄位。Day 5 會深入探討請求驗證的進階主題。
自動說明頁面:Swagger UI 與 ReDoc
FastAPI 自動產生兩種 API 說明頁面:Swagger UI(互動式,可直接從瀏覽器發請求)與 ReDoc(唯讀,規格導向)。這兩份頁面都從你寫的 Python 程式碼(型別提示、Pydantic 模型、docstring)生出來,所以永遠跟程式同步,不會有說明過時的問題。
讓說明頁面更實用的小技巧:
# 用 docstring 與 response_model 讓說明更清楚
from fastapi import FastAPI, status
from pydantic import BaseModel, Field
app = FastAPI(
title="書本管理 API",
description="練習 FastAPI 的書本 CRUD 範例",
version="0.1.0",
# 可以在 docs 頁加聯絡資訊、授權等 metadata
contact={"name": "Hao", "url": "https://blog.hao-code.com/"},
license_info={"name": "MIT"},
)
class Book(BaseModel):
id: int
title: str = Field(min_length=1, max_length=200)
author: str = Field(min_length=1, max_length=80)
year: int = Field(ge=0, le=2100)
class BookCreate(BaseModel):
title: str = Field(min_length=1, max_length=200)
author: str = Field(min_length=1, max_length=80)
year: int = Field(ge=0, le=2100)
@app.get(
"/books/{book_id}",
response_model=Book,
summary="取得單本書",
description="依據 ID 取得一本書的詳細資料。如果找不到會回 404。",
responses={404: {"description": "找不到這本書"}},
)
def get_book(book_id: int):
"""取得單本書的詳細資料。
Args:
book_id: 書本的唯一識別碼。
Returns:
Book 物件。
Raises:
HTTPException: 找不到時回 404。
"""
# ... 實作略
return Book(id=book_id, title="placeholder", author="placeholder", year=2024)
summary、description、response_model、responses 這些參數會直接反映到 Swagger UI 上;docstring 也會被擷取。實務上建議每個端點都寫簡短的 summary 與 description,這對前後端溝通非常有幫助。response_model 除了影響說明頁面,還會被 FastAPI 用來「過濾回應」——例如 UserPublic 沒列的密碼欄位,即使你的函式不小心回傳了,也不會被送到呼叫端。這是一道重要的安全防線,Day 10 會再深入。
完整實作:書本管理 API
把今天所有東西一次做完。我們建立一個完整的書本 CRUD API,包含型別提示、Pydantic 模型、查詢參數篩選、自動說明、最後用 httpx 走完整個生命週期:
建立 src/book_api/main.py:
# src/book_api/main.py
# 書本管理 API(FastAPI + Pydantic v2)
from typing import Annotated
from fastapi import FastAPI, HTTPException, Query, status
from pydantic import BaseModel, ConfigDict, Field
app = FastAPI(
title="書本管理 API",
description="練習 FastAPI 的書本 CRUD 範例",
version="0.1.0",
)
# ---------- 資料模型 ----------
class BookBase(BaseModel):
title: str = Field(min_length=1, max_length=200)
author: str = Field(min_length=1, max_length=80)
year: int = Field(ge=0, le=2100)
class BookCreate(BookBase):
# 建立用:繼承 BookBase,沒有額外欄位
pass
class BookUpdate(BaseModel):
# 更新用:所有欄位都可選
title: str | None = Field(default=None, min_length=1, max_length=200)
author: str | None = Field(default=None, min_length=1, max_length=80)
year: int | None = Field(default=None, ge=0, le=2100)
class Book(BookBase):
id: int
model_config = ConfigDict(from_attributes=True)
# ---------- 假資料(Day 6 會換成 SQLite) ----------
_books: dict[int, Book] = {
1: Book(id=1, title="Python 程式設計實戰", author="Hao", year=2024),
2: Book(id=2, title="FastAPI 入門", author="Wei", year=2025),
}
_next_id = 3
# ---------- 端點 ----------
@app.get("/books", response_model=list[Book], summary="列出書本")
def list_books(
author: Annotated[str | None, Query(description="依作者篩選")] = None,
limit: Annotated[int, Query(ge=1, le=100)] = 10,
):
items = list(_books.values())
if author is not None:
items = [b for b in items if b.author == author]
return items[:limit]
@app.get("/books/{book_id}", response_model=Book, summary="取得單本書")
def get_book(book_id: int):
book = _books.get(book_id)
if book is None:
raise HTTPException(status_code=404, detail="book not found")
return book
@app.post(
"/books",
response_model=Book,
status_code=status.HTTP_201_CREATED,
summary="建立新書",
)
def create_book(payload: BookCreate):
global _next_id
new = Book(id=_next_id, **payload.model_dump())
_books[_next_id] = new
_next_id += 1
return new
@app.patch("/books/{book_id}", response_model=Book, summary="更新書本")
def update_book(book_id: int, payload: BookUpdate):
book = _books.get(book_id)
if book is None:
raise HTTPException(status_code=404, detail="book not found")
# 只更新使用者提供的欄位
data = book.model_dump()
for key, value in payload.model_dump(exclude_unset=True).items():
data[key] = value
updated = Book(**data)
_books[book_id] = updated
return updated
@app.delete(
"/books/{book_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="刪除書本",
)
def delete_book(book_id: int):
if book_id not in _books:
raise HTTPException(status_code=404, detail="book not found")
del _books[book_id]
return None
# 啟動:uvicorn book_api.main:app --reload --port 8000
這個範例做了幾件事:
- 用
Annotated[type, Query(...)]寫查詢參數,這是 FastAPI 0.95+ 推薦的新寫法,型別與驗證參數分開,比name: int = Query(1, ge=1)更清楚。 - PATCH 用
exclude_unset=True只取使用者真的有給的欄位,避免把選填欄位用None覆蓋掉原本的值。 status.HTTP_201_CREATED、status.HTTP_204_NO_CONTENT這些常數比直接寫201、204更易讀。response_model明確指定回應模型,讓 Swagger UI 與自動說明頁面都能正確顯示回應結構。
啟動並用 httpx 走完整個 CRUD:
# tests/test_book_cycle.py
import httpx
BASE = "http://127.0.0.1:8000"
def main():
with httpx.Client(base_url=BASE, timeout=5.0) as client:
# 列出所有書
r = client.get("/books")
print("GET /books ->", r.status_code, r.json())
# 輸出(範例):GET /books -> 200 [{'id': 1, ...}, {'id': 2, ...}]
# 篩選 + 限制筆數
r = client.get("/books", params={"author": "Hao", "limit": 5})
print("GET /books?author=Hao ->", r.status_code, r.json())
# 輸出(範例):GET /books?author=Hao -> 200 [{'id': 1, 'title': 'Python 程式設計實戰', ...}]
# 建立新書
new = {"title": "資料庫設計", "author": "Hao", "year": 2025}
r = client.post("/books", json=new)
print("POST /books ->", r.status_code, r.json())
# 輸出(範例):POST /books -> 201 {'id': 3, 'title': '資料庫設計', 'author': 'Hao', 'year': 2025}
book_id = r.json()["id"]
# 部分更新(只改 year)
r = client.patch(f"/books/{book_id}", json={"year": 2026})
print(f"PATCH /books/{book_id} ->", r.status_code, r.json())
# 輸出(範例):PATCH /books/3 -> 200 {'id': 3, 'title': '資料庫設計', 'author': 'Hao', 'year': 2026}
# 驗證請求錯誤(year 太大)
r = client.post("/books", json={"title": "x", "author": "y", "year": 9999})
print("POST /books (year=9999) ->", r.status_code, r.json())
# 輸出(節錄):POST /books (year=9999) -> 422 {'detail': [{'type': 'less_than_equal', 'loc': ['body', 'year'], ...}]}
# 刪除
r = client.delete(f"/books/{book_id}")
print(f"DELETE /books/{book_id} ->", r.status_code)
# 輸出(範例):DELETE /books/3 -> 204
main()
這個迴圈展示「正常流程 + 錯誤流程」兩條路線:建立、更新用 201、200,錯誤輸入用 422,刪除用 204,每個狀態碼都有對應的語意。實務上你會把這些測試包成 test_collection,並用 FastAPI 的 TestClient(Day 16)做更系統化的測試。
常見錯誤與踩雷
第一個常見的踩雷是「Pydantic v1 vs v2 寫法混用」。在 2024 年之前很多教學用的是 Pydantic v1 的 @validator、Config inner class;v2 改成 @field_validator、model_config = ConfigDict(...)。兩者不相容,直接複製舊教學會讓 FastAPI 報錯。如果你看到錯誤訊息提到 validator、Config,先確認你用的是哪一版。本系列固定使用 Pydantic v2,規格書也以 2.11 為基準。
第二個是「忘了裝 Annotated」。FastAPI 0.95 之後推薦用 Annotated[type, Query()] 寫查詢參數;但有些舊教學仍寫 name: int = Query(1),這在新版也還能用,但失去了型別與驗證分離的好處。建議一律用 Annotated 寫法,說明頁面更乾淨。
第三個是「response_model 與回傳值型別衝突時 FastAPI 會自動過濾」。如果你的函式回傳了一個 User 物件(含密碼雜湊)但 response_model=UserPublic,FastAPI 會把密碼欄位過濾掉。這通常是好事,但偶爾會讓除錯變困難:你想看密碼欄位卻看不到。除錯時可以暫時把 response_model 拿掉,確認完整資料是否真的有送出來。
效能與實務提醒
FastAPI 的效能優勢來自 Starlette(ASGI 非同步)與 Pydantic v2(用 Rust 寫的驗證核心)。在本機簡單測試下,FastAPI 0.116 的吞吐量比 Flask 高三到五倍,比 Django REST Framework 高十倍以上。但這些數字都是「Hello World 等級」的對比,實務上的瓶頸通常在資料庫查詢、I/O 與第三方 API 呼叫,框架本身的效能差異往往只佔 10% 以下。Day 18 會示範怎麼用非同步 I/O 把 I/O 密集型 API 的吞吐量再拉上去。
response_model 的過濾是有代價的。當 response_model 是複雜的巢狀結構,Pydantic 會做一次驗證與序列化;對於非常大的回應(例如一次回傳一萬筆資料),這個步驟會佔用可觀的 CPU。這種情境可以考慮改成「stream 回應」或「分頁」(Day 7 會詳細講)。
Swagger UI 在開發階段非常好用,但正式環境通常不希望把整份 API 暴露在外。可以透過環境變數(Day 29)動態決定是否掛載 /docs:
# 依環境變數決定是否顯示說明頁面
from fastapi import FastAPI
import os
app = FastAPI(docs=None if os.getenv("ENV") == "production" else "/docs")
# 上線時設定 ENV=production 就會關閉 /docs
小結
今天我們正式進入 FastAPI 的核心:路由用裝飾器組合 URL 與 HTTP 方法、Pydantic v2 模型用型別提示與 Field 約束做嚴格驗證、response_model 與自動參數描述讓 Swagger UI 與 ReDoc 從程式碼生出來。我們做了一個完整的「書本管理 API」,支援串列、單筆讀取、建立、部分更新、刪除五個端點,並展示錯誤輸入會自動回 422 的標準行為。這些機制會在後續每天反覆出現,是整個系列最常被用到的 FastAPI 子集合。
進階範例:用 Pydantic 做複雜的巢狀模型
真實世界的 API 經常需要處理巢狀的資料結構:例如「訂單裡有多個訂單細項,每個細項裡又有商品資訊」。Pydantic v2 把這件事做得非常自然——只要把其他 Pydantic 模型當作欄位型別,就能自動遞迴驗證與序列化。
# nested_models.py
# Pydantic v2 的巢狀模型示範
from pydantic import BaseModel, Field
class Author(BaseModel):
name: str = Field(min_length=1, max_length=80)
email: str = Field(pattern=r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
class Book(BaseModel):
title: str = Field(min_length=1, max_length=200)
author: Author # 巢狀:直接用另一個 Pydantic 模型當欄位型別
year: int = Field(ge=0, le=2100)
# 建立並驗證
book = Book(
title="Python 程式設計實戰",
author=Author(name="Hao", email="hao@example.com"),
year=2024,
)
print(book.model_dump())
# 輸出(範例):
# {
# 'title': 'Python 程式設計實戰',
# 'author': {'name': 'Hao', 'email': 'hao@example.com'},
# 'year': 2024
# }
# 巢狀驗證失敗會傳到正確的欄位
try:
Book(
title="壞的書",
author=Author(name="", email="not-email"), # name 太短、email 格式錯
year=2024,
)
except Exception as e:
print("驗證失敗:", type(e).__name__)
# 輸出:驗證失敗: ValidationError
這支範例展示 Pydantic 巢狀模型的兩個重點:第一,Author 是一個獨立的 Pydantic class,可以單獨使用(例如「只回傳作者資料」的端點);第二,巢狀驗證會把所有錯誤一次列出,包括 author.name 太短與 author.email 格式不對。這對 API 的「錯誤回應結構化」設計非常有幫助——前端可以直接對應到表單欄位,不需自己判斷哪裡錯了。
第二個進階主題是「用 Pydantic 做設定管理」。後端專案通常需要讀一堆設定(資料庫連線、API key、功能開關),把這些設定寫在環境變數或設定檔後,讓 Pydantic 在啟動時一次驗證所有值是合法、是極佳的設計:
# settings.py
# 用 Pydantic BaseSettings 管理後端設定(pydantic-settings 套件)
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppSettings(BaseSettings):
# 從環境變數讀取,預設值是開發用
app_name: str = "book-api"
debug: bool = False
database_url: str = Field(default="sqlite:///./books.db")
api_key: str | None = Field(default=None, description="外部 API 金鑰")
max_connections: int = Field(default=10, ge=1, le=100)
model_config = SettingsConfigDict(env_file=".env", env_prefix="BOOK_")
# 會從 BOOK_APP_NAME、BOOK_DEBUG 這些環境變數讀取
# 也可以寫在 .env 檔裡
# 用法
settings = AppSettings()
print(f"應用名稱:{settings.app_name}")
print(f"除錯模式:{settings.debug}")
print(f"資料庫 URL:{settings.database_url}")
# 輸出(範例):
# 應用名稱:book-api
# 除錯模式:False
# 資料庫 URL:sqlite:///./books.db
pydantic-settings 是 Pydantic 團隊推出的設定管理套件,把「從環境變數讀取 + 用 Pydantic 驗證格式」打包在一起。env_prefix="BOOK_" 讓所有設定都加上前綴,避免與其他系統的環境變數衝突;env_file=".env" 則讓你可以把開發用的設定寫在 .env 裡(記得放進 .gitignore!)。這個寫法在 Day 29 會正式介紹,今天先預習。
第三個進階設計是「用 Enum 管理允許值」。如果某個欄位只能接受特定的幾個值(例如書本的「類型」只能是「電子書、紙本書、有聲書」),用 Pydantic 的 Enum 比字串比對更安全。
# enum_models.py
# Pydantic 與 Enum 的搭配
from enum import Enum
from pydantic import BaseModel, Field
class BookFormat(str, Enum):
PAPERBACK = "paperback" # 平裝
HARDCOVER = "hardcover" # 精裝
EBOOK = "ebook" # 電子書
AUDIOBOOK = "audiobook" # 有聲書
class BookV2(BaseModel):
title: str = Field(min_length=1, max_length=200)
format: BookFormat = BookFormat.PAPERBACK
# 合法
b = BookV2(title="範例書", format=BookFormat.EBOOK)
print(f"{b.title} ({b.format.value})")
# 輸出:範例書 (ebook)
# 非法值會被拒絕
try:
BookV2(title="壞的書", format="video")
except Exception as e:
print("驗證失敗:", type(e).__name__)
# 輸出:驗證失敗: ValidationError
用 str, Enum 而不是純 Enum,可以讓這個 enum 同時當字串使用(直接序列化為 JSON)又保留 enum 的型別安全。當你的 API 有「狀態」、「類型」、「等級」這類受限制的欄位時,這個設計讓前後端都有清楚的對應。
結語
今天我們從「Hello World 等級的 FastAPI」走到「完整 CRUD 的書本管理 API」,但目前的資料是存在記憶體裡的 _books 字典,伺服器重啟就會消失。明天,我們會進入更實用的機制:請求驗證(涵蓋路徑參數、查詢參數、請求主體、標頭的綜合驗證)與相依性注入(FastAPI 最強大的設計之一)。你會學到怎麼把常用邏輯抽出來、用 Depends 自動注入,以及怎麼設計一個可維護的後端架構。
延伸資源
- FastAPI 官方教學(2025):
https://fastapi.tiangolo.com/tutorial/ - Pydantic v2 遷移指南(2025):
https://docs.pydantic.dev/latest/migration/ - OpenAPI 3.1 規格書(2023):
https://spec.openapis.org/oas/v3.1.0 - Starlette 官方介紹(2025):
https://www.starlette.io/ - Annotated 型別(PEP 593,2019):
https://peps.python.org/pep-0593/ - FastAPI path operation configuration(2025):
https://fastapi.tiangolo.com/tutorial/path-operation-configuration/
留言
張貼留言