FastAPI 是一个基于 Python 类型标注的现代 Web 框架,底层是 Starlette(ASGI)和 Pydantic(数据校验)。它的核心思想很纯粹:你写在函数签名上的类型标注,就是一切契约的来源——路由参数解析、请求体校验、响应序列化、自动生成的 OpenAPI 文档,全部从同一份标注推导出来。写一次标注,得到校验、文档和编辑器补全,这是它对「动态语言写后端」这件事的最大重构。

安装与第一个应用

FastAPI 本身只是个 ASGI 应用,需要搭配一个服务器才能跑起来,最常用的是 uvicorn

1
pip install fastapi "uvicorn[standard]"

最小应用只需一个文件(main.py):

1
2
3
4
5
6
7
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def root():
return {"message": "hello, fastapi"}

启动开发服务器,--reload 开启热重载:

1
uvicorn main:app --reload

main:app 的含义是「main.py 文件里的 app 对象」。启动后访问 http://127.0.0.1:8000/ 即可看到 JSON 响应。FastAPI 最让人省心的地方是开箱即用的交互式文档:

  • http://127.0.0.1:8000/docs —— Swagger UI,可直接发请求调试
  • http://127.0.0.1:8000/redoc —— ReDoc,适合阅读
  • http://127.0.0.1:8000/openapi.json —— 原始 OpenAPI 规范

这些文档不是手写的,而是从你的路由签名和 Pydantic 模型自动生成的。

路径参数与查询参数

把类型标注加到路径参数上,FastAPI 会自动做类型转换与校验:

1
2
3
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}

访问 /items/3 返回 {"item_id": 3}(注意是数字而非字符串);访问 /items/abc 则直接返回 422 错误,并附上详细的校验失败原因——你不需要手写任何校验代码。

查询参数就是「出现在函数签名里、但不在路径字符串里的参数」,默认值让它变为可选:

1
2
3
@app.get("/items")
def list_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}

需要更细的约束时,用 Query

1
2
3
4
5
6
7
8
9
from fastapi import Query

@app.get("/items")
def list_items(
q: str | None = Query(None, max_length=50, min_length=1),
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
):
return {"q": q, "skip": skip, "limit": limit}

ge/gt/le/lt 分别是 ≥、>、≤、<。这些约束同样会写进 OpenAPI 文档,前端能直接看到。

注意一个约定:Query(None, ...) 表示「可选,默认 None」;Query(..., ...) 中的 ...(Ellipsis)表示「必填」。两者别混淆。

请求体:Pydantic 模型

GET 参数适合零散字段,POST/PUT 的复杂数据用 Pydantic 模型描述。定义一个继承 BaseModel 的类,把它作为参数类型即可:

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

class Item(BaseModel):
name: str
price: float
is_offer: bool | None = None # 可选字段,默认 None

@app.post("/items")
def create_item(item: Item):
return item

发一个 JSON 请求体,FastAPI 自动反序列化成 Item 实例;字段缺失或类型不符,照样返回 422 + 错误详情。模型可以嵌套,描述树形结构毫无压力:

1
2
3
4
5
6
7
class Image(BaseModel):
url: str
name: str

class Item(BaseModel):
name: str
images: list[Image] = [] # 嵌套模型列表

数据校验:Field 与约束

Field 给字段附加约束和元信息,是 Query 的模型字段版:

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

class Item(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
price: float = Field(..., gt=0, description="必须为正数")
tax: float | None = Field(None, ge=0)

@app.post("/items")
def create_item(item: Item):
return {"name": item.name, "price_with_tax": item.price + (item.tax or 0)}

description 会进入文档页面,比手写注释更贴近接口契约。校验失败时,响应体里的 detail 数组会精确指出哪个字段、因为什么原因失败,省去了大量联调扯皮。

响应模型

默认情况下 FastAPI 会原样返回你 return 的内容。但当返回值里混着敏感字段(比如密码哈希、内部 ID)时,用一个独立的输出模型来「过滤」是更安全的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pydantic import BaseModel

class User(BaseModel):
username: str
email: str
hashed_password: str # 内部字段

class UserOut(BaseModel):
username: str
email: str # 对外只暴露这两个

@app.post("/users", response_model=UserOut)
def create_user(user: User):
return user # 多出来的 hashed_password 会被自动剔除

response_model 不仅过滤字段,还负责把 ORM 对象、字典等序列化成响应。返回 SQLAlchemy 模型时,需要在输出模型上开启 from_attributes=True(Pydantic v2 语法,v1 是 orm_mode = True),让它能从对象的属性读取数据。

异步与同步

FastAPI 同时支持 async def 和普通 def 路由,这是它性能的关键之一:

1
2
3
4
5
6
import asyncio

@app.get("/slow")
async def slow():
await asyncio.sleep(0.5) # 非阻塞等待
return {"done": True}

两种写法的分工很重要:

  • async def:适合调用异步库(httpxasyncpgaioredis)。函数在事件循环里协程式运行,期间不阻塞其他请求。
  • 普通 def:适合调用同步阻塞库(requestspymysql)。FastAPI 会把它丢到一个外部线程池里执行,不会卡住事件循环。

最常见的性能陷阱就是把同步阻塞调用塞进 async def——此时它运行在事件循环线程上,一次阻塞会拖慢所有并发请求。如果拿不准,调用同步库就用普通 def,让框架帮你兜底。

依赖注入

依赖注入(DI)是 FastAPI 的灵魂特性。任何「多个路由都要重复做的事」——查询参数解析、数据库会话、鉴权——都能抽成一个依赖函数,用 Depends 注入:

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

def common_params(q: str | None = None, skip: int = 0, limit: int = 10):
return {"q": q, "skip": skip, "limit": limit}

@app.get("/items")
def list_items(commons: dict = Depends(common_params)):
return commons

@app.get("/users")
def list_users(commons: dict = Depends(common_params)):
return commons # 两个路由共享同一份查询参数逻辑

依赖可以嵌套(依赖里再依赖),形成一棵依赖树。带 yield 的依赖则用于资源管理,类似上下文管理器:

1
2
3
4
5
6
7
8
9
10
def get_db():
db = SessionLocal()
try:
yield db # 把会话交给路由使用
finally:
db.close() # 路由返回后自动清理

@app.get("/users")
def list_users(db = Depends(get_db)):
return db.query(User).all()

鉴权也建在 DI 之上:写一个依赖校验 token,把它的返回值作为「当前用户」注入到需要保护的路由里。

路由分组:APIRouter

项目变大后,把所有路由堆在 main.py 里不可维护。APIRouter 是一个「子应用」,可以按模块拆分:

1
2
3
4
5
6
7
8
9
10
11
12
# items.py
from fastapi import APIRouter

router = APIRouter(prefix="/items", tags=["items"])

@router.get("/")
def list_items():
return []

@router.get("/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
1
2
3
4
5
6
# main.py
from fastapi import FastAPI
from items import router as items_router

app = FastAPI()
app.include_router(items_router)

prefix 给整组路由加前缀,tags 让它们在文档里归到一组。这是组织中大型 FastAPI 项目的标准做法。

中间件与 CORS

中间件包裹在整个应用外侧,每个请求进来、每个响应出去都会经过。前后端分离场景下最常用的是 CORS:

1
2
3
4
5
6
7
8
9
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com"], # 生产环境别用 "*"
allow_methods=["*"],
allow_headers=["*"],
allow_credentials=True,
)

自定义中间件用 @app.middleware("http"),能在请求前后插入逻辑(计时、日志、统一错误处理等)。

常见陷阱

async def 里调用同步阻塞库。比如 async def 里用 requests.get(...) 或同步数据库驱动,会阻塞整个事件循环。改用异步库,或把函数降级为普通 def 让线程池兜底。

路由声明顺序错误/users/me 必须声明在 /users/{user_id} 之前,否则 me 会被当成 user_id 去做 int 转换,直接 422。FastAPI 按声明顺序匹配路由,顺序就是优先级。

Pydantic v1 与 v2 的 API 差异。v2 里 class Config 换成了 model_config = ConfigDict(...).dict() / .parse_obj() 换成了 .model_dump() / .model_validate()orm_mode 换成了 from_attributes。混用两套写法会踩坑,新项目直接锁定 v2 语法。

可变默认值def f(tags: list = []) 是 Python 的经典坑,FastAPI 也不会替你免疫。用 Query(default=[])None + 函数内初始化代替。

response_model 与返回 ORM 对象。返回 SQLAlchemy 模型却没在输出模型上设 from_attributes=True,会得到「无法序列化」的错误。Pydantic 默认只接受 dict / 自身实例,要让它读对象属性必须显式开启。

...(必填)和 None(可选)搞混Query(...) 是必填,Query(None) 是可选默认空,Query(None, ...) 这种写法自相矛盾、行为不可预期。

总结

FastAPI 的设计哲学是「以类型标注为单一数据源」。掌握这一点,大部分特性都可以举一反三地推导出来。

特性关键点示例
路由参数签名上的类型标注自动转换/校验def read(item_id: int)
查询参数非路径参数 + 默认值,Query 加约束q: str | None = Query(None)
请求体Pydantic BaseModel 作参数类型def create(item: Item)
校验Field 给字段加约束与描述Field(..., gt=0)
响应response_model 过滤/序列化输出@app.post(..., response_model=UserOut)
异步async def 用异步库,def 走线程池async def slow(): await ...
依赖注入Depends 复用逻辑,yield 管资源db = Depends(get_db)
路由分组APIRouter + include_routerrouter = APIRouter(prefix="/items")
文档自动生成 /docs/redoc无需手写

上手路径建议:先用路径参数和 Pydantic 模型把 CRUD 跑通,再引入 Depends 做鉴权和数据库会话,最后用 APIRouter 拆分模块。一旦习惯了「写标注即写契约」的节奏,回到传统框架反而会觉得啰嗦。