只写几行 Python,浏览器里就出现了一套可以直接测试的接口文档;参数传错时,框架还会主动告诉我们错在哪里。这种"写完马上看到结果"的体验,正是 FastAPI 对新手最有吸引力的地方。
今天讲述一下 FastAPI 的自动接口文档、参数类型和 Pydantic 数据模型,并结合项目代码重点学习依赖注入与中间件的作用、执行顺序及使用场景。
一、自动接口文档与项目启动
截图上半部分是接口列表:蓝色 GET 用于查询,绿色 POST 用于提交。下半部分的 Schemas 来自 Pydantic 模型,点开后可以查看字段名称、类型和必填项。
python
from fastapi import FastAPI
# 创建 FastAPI 应用对象,后续路由都注册到 app 上
app = FastAPI()
# 注册一个处理 GET / 请求的路由
@app.get("/")
async def root():
# 字典会被 FastAPI 自动转换成 JSON 响应
return {"message": "Hello World"}
app 是应用对象,@app.get("/") 声明请求方式和路径,root() 负责处理请求。修改或新增路由后,/docs 中的接口也会跟着更新。
项目可以选择一种依赖管理方式启动,不要混用不同环境:
powershell
# uv:当前项目推荐
uv sync
uv run uvicorn main:app --reload
# pip
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install fastapi uvicorn
python -m uvicorn main:app --reload
# Poetry
poetry install
poetry run uvicorn main:app --reload
main:app 中,main 是模块名,app 是应用对象;--reload 会在代码变化后自动重启服务。启动成功后访问 http://127.0.0.1:8000/docs。📌
二、参数类型
FastAPI 接口常见的参数分为路径参数、查询参数和请求体参数。它们出现的位置不同,用途也不同。
路径参数
路径参数写在 URL 路径中,用来确定具体对象:
python
# 路径中的 {id} 会传给同名参数 id
@app.get("/book/{id}")
async def get_book(
# id 必须是整数,并且大于 0、小于 101
id: int = Path(..., gt=0, lt=101, description="书籍id,取值范围1-100"),
):
return {"id": id, "title": f"这是第{id}本书"}
/book/3 可以通过校验;/book/abc 不是整数,/book/101 不满足 lt=101,都会返回 422。
查询参数
查询参数写在 ? 后面。分页时,page 表示当前页,pageSize 表示每页数量:
python
@app.get("/news/news_list")
async def get_news_list(
# 不传 page 时默认为第 1 页,页码最小为 1
page: int = Query(default=1, ge=1, description="当前页码"),
# 每页默认 10 条,允许的范围是 1~60 条
pageSize: int = Query(default=10, ge=1, le=60, description="每页数量"),
):
return {"page": page, "pageSize": pageSize}
请求第 2 页、每页 20 条,可以访问 /news/news_list?page=2&pageSize=20。
🔎 路径参数回答"要找哪一个",查询参数回答"要怎么查询"。
请求体参数
POST 接口通常通过请求体接收一组 JSON。user: User 表示请求体需要符合 User 类型:
python
@app.post("/register")
async def register(user: User):
# FastAPI 已经按照 User 模型完成请求体解析和校验
return user
User 的完整定义会在下一节介绍。
常用参数约束
Path、Query 和 Field 都可以使用约束参数,FastAPI 会在执行路由前完成检查:
| 参数 | 通俗解释 | 示例 |
|---|---|---|
default |
客户端不传时采用的值 | default=10 |
... |
参数必须提供,没有默认值 | Path(...) |
min_length |
字符串最少字符数 | min_length=2 |
max_length |
字符串最多字符数 | max_length=10 |
gt |
必须大于,不包含边界值 | gt=0 |
ge |
必须大于等于 | ge=1 |
lt |
必须小于,不包含边界值 | lt=101 |
le |
必须小于等于 | le=60 |
description |
显示在 /docs 中的说明 |
description="当前页" |
💡 g 是 greater,l 是 less,e 是 equal,缩写末尾带 e 就表示包含等号。
三、Pydantic 请求与响应模型
请求模型、响应模型和注册路由放在同一个代码块中,更容易看出它们的关系:
python
# 请求模型:检查客户端提交的用户名和密码
class User(BaseModel):
username: str = Field(default="张三", min_length=2, max_length=10)
password: str = Field(min_length=3, max_length=20)
# 响应模型:只允许接口返回 username
class RegisterResponse(BaseModel):
username: str
# response_model 会检查并过滤路由的返回结果
@app.post("/register", response_model=RegisterResponse)
async def register(user: User):
# 不返回 password,避免泄露用户密码
return {"username": user.username}
user: User 用来约束请求参数的类型,response_model=RegisterResponse 用来约束响应结果的类型。
Field 的常用参数
Field 用来给模型字段补充默认值、长度、数值范围和文档说明:
| 参数 | 通俗解释 | 示例 |
|---|---|---|
default |
客户端不传时使用默认值 | default="张三" |
default_factory |
调用函数生成默认值 | default_factory=list |
min_length |
字符串最少字符数 | min_length=2 |
max_length |
字符串最多字符数 | max_length=10 |
gt |
数字必须大于指定值 | gt=0 |
ge |
数字必须大于等于指定值 | ge=1 |
lt |
数字必须小于指定值 | lt=101 |
le |
数字必须小于等于指定值 | le=100 |
description |
在 /docs 中说明字段 |
description="用户名" |
examples |
在 /docs 中提供示例值 |
examples=["小明"] |
✅ 开头截图中的
User、News等Schemas,就是 FastAPI 根据 Pydantic 模型生成的数据结构说明。
四、依赖注入的执行过程
新闻列表和用户列表都需要分页参数。为了避免重复,可以把参数提取成公共依赖:
python
# 公共依赖:集中接收并校验分页参数
async def common_parameters(
page: int = Query(1, ge=1),
pageSize: int = Query(10, le=60),
):
# 返回值会注入使用该依赖的路由参数中
return {"page": page, "pageSize": pageSize}
需要分页的路由通过 Depends 使用它:
python
# Depends 会先执行 common_parameters,再把结果传给 commons
@app.get("/news/news_list")
async def get_news_list(commons=Depends(common_parameters)):
return commons
# 多个路由可以复用同一个依赖,不必重复编写分页校验
@app.get("/user/user_list")
async def get_user_list(commons=Depends(common_parameters)):
return commons
Depends(common_parameters) 可以理解为:"执行当前路由前,先运行 common_parameters,再把结果交给我。"
请求 /user/user_list?page=2&pageSize=20 时,FastAPI 会:
- 找到目标路由,并发现其中的
Depends。 - 调用
common_parameters,读取并校验分页参数。 - 将
{"page": 2, "pageSize": 20}放入commons。 - 最后执行
get_user_list中的代码。
🧩
Depends让公共逻辑可以复用,并由 FastAPI 自动安排执行顺序。
五、中间件的执行顺序
当前项目注册了两个 HTTP 中间件:
python
# 先注册的中间件位于内层
@app.middleware("http")
async def middleware2(request, call_next):
# call_next 之前:处理进入应用的请求
print("中间件2 start")
# 把请求交给下一层中间件或路由
response = await call_next(request)
# call_next 之后:处理路由返回的响应
print("中间件2 end")
return response
# 后注册的中间件位于外层,因此请求会先进入这里
@app.middleware("http")
async def middleware1(request, call_next):
print("中间件1 start")
# 等待内层中间件和路由执行完成
response = await call_next(request)
print("中间件1 end")
# 将最终响应返回给客户端
return response
request 是当前请求,call_next 表示继续执行下一层,response 是后续流程返回的响应。await call_next(request) 之前处理进入的请求,之后处理返回的响应。
从代码位置看,middleware2 在上面,middleware1 在下面。后注册的 middleware1 会包在外层,所以顺序是:
text
middleware1 start
middleware2 start
路由执行
middleware2 end
middleware1 end
请求进入时按代码位置自下往上,响应返回时再反向执行,这就是"洋葱模型"。
🔄
start部分处理进入的请求,end部分处理返回的响应。
六、中间件与依赖注入的选择方法
常见需求可以直接通过下表判断:
| 实际需求 | 是否所有请求都需要 | 路由是否需要结果 | 选择 | 原因 |
|---|---|---|---|---|
| 记录访问日志 | 是 | 否 | 中间件 | 每个请求都要执行 |
| 统计完整请求耗时 | 是 | 否 | 中间件 | 需要包住请求与响应全过程 |
| 给所有响应添加响应头 | 是 | 否 | 中间件 | 需要在路由返回后统一修改 |
| 统一处理跨域 | 是 | 否 | 中间件 | 属于整个应用的共同规则 |
| 维护期间拦截请求 | 是 | 否 | 中间件 | 可以不调用 call_next,直接返回响应 |
| 列表接口共用分页参数 | 否 | 是 | 依赖注入 | 只有列表需要,路由还要使用结果 |
| 部分接口读取当前用户 | 否 | 是 | 依赖注入 | 用户信息要交给路由继续使用 |
| 管理接口检查权限 | 否 | 不一定 | 依赖注入 | 权限要求与具体路由有关 |
| 多个接口共用查询参数 | 否 | 是 | 依赖注入 | 可以统一校验并注入结果 |
🧭 关心"每个请求都要做什么",选择中间件;关心"这个路由需要什么",选择依赖注入。
七、结语
现在,我们已经从 /docs 看到了接口如何生成,也理清了参数类型、Pydantic、依赖注入和中间件之间的关系。文章会随着后续学习持续完善,欢迎讨论、补充与纠错。💬