摘要
FastAPI 是近年来 Python Web 开发中非常受欢迎的现代 Web 框架。它基于 Python 类型提示进行参数校验和接口描述,使用 Pydantic 处理数据模型,并运行在 ASGI 生态之上,能够较好地支持异步请求、自动生成 OpenAPI 文档和高效的 API 开发。
本文将从 Flask 与 FastAPI 的开发模型差异开始,介绍 FastAPI 的安装、路由、路径参数、查询参数、请求体、响应模型、依赖注入、错误处理和自动文档。随后通过一个任务管理 API 实战,展示如何使用异步接口、SQLite 数据库和项目分层构建一个更接近生产实践的 FastAPI 服务。
读完本文后,你应该能够:
- 理解 FastAPI 的核心设计;
- 区分 WSGI 同步模型与 ASGI 异步模型;
- 使用类型提示定义 API 参数;
- 使用 Pydantic 进行请求和响应校验;
- 使用 FastAPI 自动生成接口文档;
- 理解依赖注入和路由拆分;
- 编写同步和异步接口;
- 组织一个可扩展的 FastAPI 项目。
一、背景与问题
1. 从 Flask 到 FastAPI
Flask 适合学习 Web 基础和快速构建 API,但随着项目发展,开发者通常需要额外解决:
- 请求参数校验;
- 响应数据格式;
- API 文档;
- 异步请求;
- 依赖复用;
- 数据模型转换;
- 类型检查。
FastAPI 的设计目标,就是把这些常见能力更紧密地整合到框架中:
text
Python 类型提示
-> 参数解析
-> 数据校验
-> OpenAPI 文档
-> 编辑器提示
例如,下面的接口声明同时表达了路径参数的类型和返回类型:
python
@app.get("/users/{user_id}")
def get_user(user_id: int) -> UserResponse:
...
FastAPI 可以根据这些类型信息判断:
- user_id 必须是整数;
- 接口返回结构应该符合 UserResponse;
- 文档中应该展示什么参数;
- 参数校验失败时返回什么错误。
2. 现代后端服务的需求
一个现代 Web API 通常需要:
- 清晰的接口声明;
- 自动生成文档;
- 可靠的参数校验;
- 可复用的依赖;
- 同步和异步支持;
- 结构化错误;
- 数据库连接管理;
- 测试客户端;
- 可观测性;
- 容器化部署。
FastAPI 并不会自动解决所有生产问题,但它提供了比较清晰的基础抽象。
3. 同步和异步的区别
同步代码:
python
def get_user():
result = call_database()
return result
执行期间,当前线程会等待 call_database 返回。
异步代码:
python
async def get_user():
result = await call_database_async()
return result
当任务等待 IO 时,事件循环可以处理其他任务。
异步更适合:
- 大量网络请求;
- 多个外部 API 调用;
- WebSocket;
- 流式响应;
- 高并发 IO 服务。
异步并不意味着所有代码都必须写成 async。CPU 密集型任务仍然需要进程、线程池或专门的计算服务。
4. FastAPI 的核心组成
可以把 FastAPI 的能力概括为:
text
Starlette
-> Web 层、路由、中间件、ASGI
Pydantic
-> 数据模型、类型校验、序列化
FastAPI
-> 参数注入、依赖注入、OpenAPI 文档
它们共同组成一个适合开发 API 的框架。
二、核心概念
1. ASGI
ASGI 是 Python 异步 Web 服务接口规范,支持:
- 普通 HTTP 请求;
- 异步处理;
- WebSocket;
- 长连接;
- 流式响应。
传统 WSGI 通常以同步调用为核心:
text
服务器
-> 调用应用
-> 应用处理请求
-> 返回响应
ASGI 可以支持异步应用:
text
事件循环
-> 接收连接
-> 调度协程
-> 在等待 IO 时切换任务
-> 返回响应
FastAPI 可以运行在 Uvicorn、Hypercorn 等 ASGI 服务器上。
2. 路由
FastAPI 使用装饰器声明路由:
python
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def index():
return {
"message": "Hello FastAPI"
}
常见路由装饰器:
python
@app.get("/items")
@app.post("/items")
@app.put("/items/{item_id}")
@app.patch("/items/{item_id}")
@app.delete("/items/{item_id}")
路径和方法清晰表达了资源操作。
3. 路径参数
python
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {
"user_id": user_id
}
访问:
text
GET /users/10
user_id 会被解析为整数。如果访问:
text
GET /users/abc
FastAPI 会自动返回参数校验错误。
4. 查询参数
python
@app.get("/items")
async def list_items(
page: int = 1,
page_size: int = 20,
keyword: str | None = None,
):
return {
"page": page,
"page_size": page_size,
"keyword": keyword,
}
查询:
text
GET /items?page=2&page_size=10&keyword=python
还可以通过 Query 定义约束:
python
from fastapi import Query
@app.get("/items")
async def list_items(
page: int = Query(
default=1,
ge=1,
),
page_size: int = Query(
default=20,
ge=1,
le=100,
),
):
return {
"page": page,
"page_size": page_size,
}
5. 请求体模型
使用 Pydantic 定义请求模型:
python
from pydantic import BaseModel, Field
class TodoCreate(BaseModel):
title: str = Field(
min_length=1,
max_length=100,
)
description: str = Field(
default="",
max_length=1000,
)
接口声明:
python
@app.post("/todos")
async def create_todo(
payload: TodoCreate,
):
return {
"title": payload.title,
"description": payload.description,
}
FastAPI 会自动:
- 读取 JSON;
- 转换字段类型;
- 检查必填字段;
- 检查长度;
- 返回校验错误;
- 将模型展示到 OpenAPI 文档。
6. 响应模型
python
class TodoResponse(BaseModel):
id: int
title: str
description: str
completed: bool
@app.get(
"/todos/{todo_id}",
response_model=TodoResponse,
)
async def get_todo(todo_id: int):
return {
"id": todo_id,
"title": "学习 FastAPI",
"description": "",
"completed": False,
"internal_field": "不会返回",
}
response_model 可以过滤未声明的字段,避免内部数据意外泄露。
7. 依赖注入
FastAPI 使用 Depends 声明依赖:
python
from fastapi import Depends
async def get_current_user():
return {
"id": 1,
"name": "Alice",
}
@app.get("/profile")
async def profile(
user=Depends(get_current_user),
):
return user
依赖可以用于:
- 数据库会话;
- 当前用户;
- 权限检查;
- 分页参数;
- 配置对象;
- 外部客户端;
- 请求上下文。
8. 自动文档
启动 FastAPI 后,默认可以访问:
text
/docs
/redoc
/openapi.json
OpenAPI 文档会根据:
- 路由;
- 方法;
- 参数类型;
- Pydantic 模型;
- 响应模型;
- 描述信息;
自动生成。
9. HTTPException
业务中可以抛出 HTTPException:
python
from fastapi import HTTPException
@app.get("/todos/{todo_id}")
async def get_todo(todo_id: int):
todo = find_todo(todo_id)
if todo is None:
raise HTTPException(
status_code=404,
detail="待办事项不存在",
)
return todo
对于大型项目,可以进一步统一业务异常和错误响应格式。
三、工作原理
1. FastAPI 请求处理流程
#mermaid-svg-fkZxk5shCIh37wTU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fkZxk5shCIh37wTU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fkZxk5shCIh37wTU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fkZxk5shCIh37wTU .error-icon{fill:#552222;}#mermaid-svg-fkZxk5shCIh37wTU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fkZxk5shCIh37wTU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fkZxk5shCIh37wTU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fkZxk5shCIh37wTU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fkZxk5shCIh37wTU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fkZxk5shCIh37wTU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fkZxk5shCIh37wTU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fkZxk5shCIh37wTU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fkZxk5shCIh37wTU .marker.cross{stroke:#333333;}#mermaid-svg-fkZxk5shCIh37wTU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fkZxk5shCIh37wTU p{margin:0;}#mermaid-svg-fkZxk5shCIh37wTU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fkZxk5shCIh37wTU .cluster-label text{fill:#333;}#mermaid-svg-fkZxk5shCIh37wTU .cluster-label span{color:#333;}#mermaid-svg-fkZxk5shCIh37wTU .cluster-label span p{background-color:transparent;}#mermaid-svg-fkZxk5shCIh37wTU .label text,#mermaid-svg-fkZxk5shCIh37wTU span{fill:#333;color:#333;}#mermaid-svg-fkZxk5shCIh37wTU .node rect,#mermaid-svg-fkZxk5shCIh37wTU .node circle,#mermaid-svg-fkZxk5shCIh37wTU .node ellipse,#mermaid-svg-fkZxk5shCIh37wTU .node polygon,#mermaid-svg-fkZxk5shCIh37wTU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fkZxk5shCIh37wTU .rough-node .label text,#mermaid-svg-fkZxk5shCIh37wTU .node .label text,#mermaid-svg-fkZxk5shCIh37wTU .image-shape .label,#mermaid-svg-fkZxk5shCIh37wTU .icon-shape .label{text-anchor:middle;}#mermaid-svg-fkZxk5shCIh37wTU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fkZxk5shCIh37wTU .rough-node .label,#mermaid-svg-fkZxk5shCIh37wTU .node .label,#mermaid-svg-fkZxk5shCIh37wTU .image-shape .label,#mermaid-svg-fkZxk5shCIh37wTU .icon-shape .label{text-align:center;}#mermaid-svg-fkZxk5shCIh37wTU .node.clickable{cursor:pointer;}#mermaid-svg-fkZxk5shCIh37wTU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fkZxk5shCIh37wTU .arrowheadPath{fill:#333333;}#mermaid-svg-fkZxk5shCIh37wTU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fkZxk5shCIh37wTU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fkZxk5shCIh37wTU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fkZxk5shCIh37wTU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fkZxk5shCIh37wTU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fkZxk5shCIh37wTU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fkZxk5shCIh37wTU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fkZxk5shCIh37wTU .cluster text{fill:#333;}#mermaid-svg-fkZxk5shCIh37wTU .cluster span{color:#333;}#mermaid-svg-fkZxk5shCIh37wTU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fkZxk5shCIh37wTU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fkZxk5shCIh37wTU rect.text{fill:none;stroke-width:0;}#mermaid-svg-fkZxk5shCIh37wTU .icon-shape,#mermaid-svg-fkZxk5shCIh37wTU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fkZxk5shCIh37wTU .icon-shape p,#mermaid-svg-fkZxk5shCIh37wTU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fkZxk5shCIh37wTU .icon-shape .label rect,#mermaid-svg-fkZxk5shCIh37wTU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fkZxk5shCIh37wTU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fkZxk5shCIh37wTU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fkZxk5shCIh37wTU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP 请求
ASGI 服务器
FastAPI 应用
路由匹配
解析路径和查询参数
解析请求体
执行依赖
调用处理函数
校验响应模型
序列化响应
返回客户端
参数校验和依赖解析发生在处理函数执行前。响应模型处理发生在业务函数返回之后。
2. 同步函数和异步函数
FastAPI 可以同时定义同步和异步处理函数:
python
@app.get("/sync")
def sync_endpoint():
return {
"mode": "sync"
}
@app.get("/async")
async def async_endpoint():
return {
"mode": "async"
}
如果处理函数是普通 def,框架通常会将其放到线程池中执行,避免阻塞事件循环。如果处理函数是 async def,则在事件循环中运行。
注意:不要在 async def 中直接执行长时间阻塞的同步代码:
python
@app.get("/bad")
async def bad_endpoint():
result = blocking_database_call()
return result
更好的选择:
- 使用异步数据库驱动;
- 将同步函数放入线程池;
- 将耗时任务放到后台队列;
- 使用专门的计算服务。
3. 依赖解析
依赖注入可以形成依赖树:
#mermaid-svg-K00UoDtdCiqCsep7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-K00UoDtdCiqCsep7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-K00UoDtdCiqCsep7 .error-icon{fill:#552222;}#mermaid-svg-K00UoDtdCiqCsep7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-K00UoDtdCiqCsep7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-K00UoDtdCiqCsep7 .marker.cross{stroke:#333333;}#mermaid-svg-K00UoDtdCiqCsep7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-K00UoDtdCiqCsep7 p{margin:0;}#mermaid-svg-K00UoDtdCiqCsep7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-K00UoDtdCiqCsep7 .cluster-label text{fill:#333;}#mermaid-svg-K00UoDtdCiqCsep7 .cluster-label span{color:#333;}#mermaid-svg-K00UoDtdCiqCsep7 .cluster-label span p{background-color:transparent;}#mermaid-svg-K00UoDtdCiqCsep7 .label text,#mermaid-svg-K00UoDtdCiqCsep7 span{fill:#333;color:#333;}#mermaid-svg-K00UoDtdCiqCsep7 .node rect,#mermaid-svg-K00UoDtdCiqCsep7 .node circle,#mermaid-svg-K00UoDtdCiqCsep7 .node ellipse,#mermaid-svg-K00UoDtdCiqCsep7 .node polygon,#mermaid-svg-K00UoDtdCiqCsep7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-K00UoDtdCiqCsep7 .rough-node .label text,#mermaid-svg-K00UoDtdCiqCsep7 .node .label text,#mermaid-svg-K00UoDtdCiqCsep7 .image-shape .label,#mermaid-svg-K00UoDtdCiqCsep7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-K00UoDtdCiqCsep7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-K00UoDtdCiqCsep7 .rough-node .label,#mermaid-svg-K00UoDtdCiqCsep7 .node .label,#mermaid-svg-K00UoDtdCiqCsep7 .image-shape .label,#mermaid-svg-K00UoDtdCiqCsep7 .icon-shape .label{text-align:center;}#mermaid-svg-K00UoDtdCiqCsep7 .node.clickable{cursor:pointer;}#mermaid-svg-K00UoDtdCiqCsep7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-K00UoDtdCiqCsep7 .arrowheadPath{fill:#333333;}#mermaid-svg-K00UoDtdCiqCsep7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-K00UoDtdCiqCsep7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-K00UoDtdCiqCsep7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K00UoDtdCiqCsep7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-K00UoDtdCiqCsep7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K00UoDtdCiqCsep7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-K00UoDtdCiqCsep7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-K00UoDtdCiqCsep7 .cluster text{fill:#333;}#mermaid-svg-K00UoDtdCiqCsep7 .cluster span{color:#333;}#mermaid-svg-K00UoDtdCiqCsep7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-K00UoDtdCiqCsep7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-K00UoDtdCiqCsep7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-K00UoDtdCiqCsep7 .icon-shape,#mermaid-svg-K00UoDtdCiqCsep7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K00UoDtdCiqCsep7 .icon-shape p,#mermaid-svg-K00UoDtdCiqCsep7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-K00UoDtdCiqCsep7 .icon-shape .label rect,#mermaid-svg-K00UoDtdCiqCsep7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K00UoDtdCiqCsep7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-K00UoDtdCiqCsep7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-K00UoDtdCiqCsep7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 路由处理函数
当前用户依赖
数据库会话依赖
认证 Token 依赖
数据库连接池
FastAPI 会按照依赖关系解析参数,处理共享依赖,并在请求结束后清理资源。
4. Pydantic 的作用
Pydantic 模型主要用于:
- 数据解析;
- 类型转换;
- 参数校验;
- 序列化;
- OpenAPI Schema 生成。
例如:
python
class UserCreate(BaseModel):
username: str = Field(
min_length=3,
max_length=50,
)
age: int = Field(
ge=0,
le=150,
)
email: str
传入不符合约束的数据时,接口会返回结构化错误。
Pydantic 模型适合表达 API 边界,但不要把所有复杂业务规则都塞进字段校验中。跨字段、依赖数据库状态的规则仍然应该放在服务层。
5. 生命周期事件
应用启动和关闭时可能需要初始化和释放资源:
- 创建数据库连接池;
- 初始化 HTTP 客户端;
- 加载配置;
- 关闭连接;
- 释放线程池;
- 保存统计信息。
可以使用 lifespan:
python
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.http_client = create_http_client()
yield
await app.state.http_client.aclose()
app = FastAPI(
lifespan=lifespan
)
资源应该在应用生命周期内复用,不要每个请求都重新创建连接池。
6. 中间件
中间件可以在请求前后执行通用逻辑:
python
import time
from starlette.middleware.base import (
BaseHTTPMiddleware,
)
class RequestTimingMiddleware(
BaseHTTPMiddleware
):
async def dispatch(
self,
request,
call_next,
):
started = time.perf_counter()
response = await call_next(request)
elapsed = (
time.perf_counter()
- started
) * 1000
response.headers[
"X-Process-Time-Ms"
] = f"{elapsed:.2f}"
return response
中间件适合处理:
- 请求 ID;
- 日志;
- CORS;
- 认证前置检查;
- 指标;
- 全局响应头;
- 统一异常记录。
四、实战示例
1. 创建项目
创建虚拟环境:
bash
mkdir fastapi-todo-api
cd fastapi-todo-api
python -m venv .venv
激活环境后安装:
bash
python -m pip install fastapi uvicorn
为了使用 SQLite 异步访问,再安装:
bash
python -m pip install aiosqlite
保存依赖:
bash
python -m pip freeze > requirements.txt
2. 项目结构
text
fastapi-todo-api/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── database.py
│ ├── dependencies.py
│ └── todos/
│ ├── __init__.py
│ ├── router.py
│ ├── schemas.py
│ ├── service.py
│ └── repository.py
├── tests/
│ └── test_todos.py
└── requirements.txt
3. 创建应用
app/main.py:
python
from fastapi import FastAPI
from app.todos.router import router as todo_router
app = FastAPI(
title="Todo API",
version="1.0.0",
description="一个 FastAPI 待办事项服务",
)
app.include_router(
todo_router,
prefix="/api/todos",
tags=["todos"],
)
@app.get("/health")
async def health():
return {
"status": "ok"
}
启动:
bash
uvicorn app.main:app --reload
访问:
text
http://127.0.0.1:8000/docs
4. 定义 Pydantic 模型
app/todos/schemas.py:
python
from datetime import datetime
from pydantic import (
BaseModel,
ConfigDict,
Field,
)
class TodoCreate(BaseModel):
title: str = Field(
min_length=1,
max_length=100,
)
description: str = Field(
default="",
max_length=1000,
)
class TodoUpdate(BaseModel):
title: str | None = Field(
default=None,
min_length=1,
max_length=100,
)
description: str | None = Field(
default=None,
max_length=1000,
)
completed: bool | None = None
class TodoResponse(BaseModel):
model_config = ConfigDict(
from_attributes=True
)
id: int
title: str
description: str
completed: bool
created_at: datetime
updated_at: datetime
class TodoListResponse(BaseModel):
items: list[TodoResponse]
total: int
page: int
page_size: int
请求模型和响应模型分开,可以避免客户端提交或看到不应该暴露的字段。
5. 初始化数据库
app/database.py:
python
from pathlib import Path
import aiosqlite
DATABASE_PATH = Path("todo.db")
async def init_db():
async with aiosqlite.connect(
DATABASE_PATH
) as db:
await db.execute(
"""
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"""
)
await db.commit()
在应用启动时初始化:
python
from contextlib import asynccontextmanager
from app.database import init_db
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
yield
将 lifespan 传入应用:
python
app = FastAPI(
title="Todo API",
lifespan=lifespan,
)
6. 实现数据访问层
app/todos/repository.py:
python
from datetime import datetime, timezone
import aiosqlite
from app.database import DATABASE_PATH
def now_utc():
return datetime.now(
timezone.utc
)
class TodoRepository:
async def create(
self,
title: str,
description: str,
) -> dict:
timestamp = now_utc().isoformat()
async with aiosqlite.connect(
DATABASE_PATH
) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute(
"""
INSERT INTO todos (
title,
description,
completed,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?)
""",
(
title,
description,
0,
timestamp,
timestamp,
),
)
await db.commit()
return await self.get_by_id(
cursor.lastrowid
)
async def get_by_id(
self,
todo_id: int,
) -> dict | None:
async with aiosqlite.connect(
DATABASE_PATH
) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute(
"""
SELECT id, title, description,
completed, created_at,
updated_at
FROM todos
WHERE id = ?
""",
(todo_id,),
)
row = await cursor.fetchone()
if row is None:
return None
return dict(row)
async def list(
self,
offset: int,
limit: int,
completed: bool | None = None,
) -> tuple[list[dict], int]:
async with aiosqlite.connect(
DATABASE_PATH
) as db:
db.row_factory = aiosqlite.Row
if completed is None:
where = ""
params = ()
else:
where = "WHERE completed = ?"
params = (int(completed),)
count_cursor = await db.execute(
f"""
SELECT COUNT(*)
FROM todos
{where}
""",
params,
)
total = (
await count_cursor.fetchone()
)[0]
cursor = await db.execute(
f"""
SELECT id, title, description,
completed, created_at,
updated_at
FROM todos
{where}
ORDER BY id DESC
LIMIT ? OFFSET ?
""",
params + (limit, offset),
)
rows = await cursor.fetchall()
return [
dict(row)
for row in rows
], total
where 条件是程序内部固定字符串,查询参数仍然使用绑定参数。复杂项目应该使用更成熟的数据库会话和连接池管理方案。
7. 实现更新和删除
python
async def update(
self,
todo_id: int,
values: dict,
) -> dict | None:
if not values:
return await self.get_by_id(todo_id)
values["updated_at"] = now_utc().isoformat()
fields = []
params = []
allowed_fields = {
"title",
"description",
"completed",
"updated_at",
}
for field, value in values.items():
if field not in allowed_fields:
continue
fields.append(f"{field} = ?")
params.append(value)
params.append(todo_id)
async with aiosqlite.connect(
DATABASE_PATH
) as db:
await db.execute(
f"""
UPDATE todos
SET {", ".join(fields)}
WHERE id = ?
""",
tuple(params),
)
await db.commit()
return await self.get_by_id(todo_id)
async def delete(
self,
todo_id: int,
) -> bool:
async with aiosqlite.connect(
DATABASE_PATH
) as db:
cursor = await db.execute(
"""
DELETE FROM todos
WHERE id = ?
""",
(todo_id,),
)
await db.commit()
return cursor.rowcount > 0
动态字段名不能使用 SQL 参数绑定,因此必须通过 allowed_fields 白名单限制。不能直接把客户端提交的字段名拼接到 SQL 中。
8. 实现服务层
app/todos/service.py:
python
from fastapi import HTTPException
from app.todos.repository import TodoRepository
class TodoService:
def __init__(
self,
repository: TodoRepository,
):
self.repository = repository
async def create(
self,
title: str,
description: str,
):
return await self.repository.create(
title=title.strip(),
description=description.strip(),
)
async def get(self, todo_id: int):
todo = await self.repository.get_by_id(
todo_id
)
if todo is None:
raise HTTPException(
status_code=404,
detail="待办事项不存在",
)
return todo
async def update(
self,
todo_id: int,
values: dict,
):
todo = await self.repository.get_by_id(
todo_id
)
if todo is None:
raise HTTPException(
status_code=404,
detail="待办事项不存在",
)
return await self.repository.update(
todo_id,
values,
)
async def delete(self, todo_id: int):
deleted = await self.repository.delete(
todo_id
)
if not deleted:
raise HTTPException(
status_code=404,
detail="待办事项不存在",
)
服务层负责业务规则和资源不存在处理,路由层只负责参数接收和响应转换。
9. 依赖注入数据库服务
app/dependencies.py:
python
from typing import Annotated
from fastapi import Depends
from app.todos.repository import TodoRepository
from app.todos.service import TodoService
def get_todo_repository():
return TodoRepository()
def get_todo_service(
repository: Annotated[
TodoRepository,
Depends(get_todo_repository),
],
):
return TodoService(repository)
10. 定义路由
app/todos/router.py:
python
from typing import Annotated
from fastapi import (
APIRouter,
Depends,
Query,
Response,
status,
)
from app.dependencies import (
get_todo_service,
)
from app.todos.schemas import (
TodoCreate,
TodoListResponse,
TodoResponse,
TodoUpdate,
)
from app.todos.service import TodoService
router = APIRouter()
@router.post(
"",
response_model=TodoResponse,
status_code=status.HTTP_201_CREATED,
)
async def create_todo(
payload: TodoCreate,
service: Annotated[
TodoService,
Depends(get_todo_service),
],
):
return await service.create(
title=payload.title,
description=payload.description,
)
@router.get(
"",
response_model=TodoListResponse,
)
async def list_todos(
service: Annotated[
TodoService,
Depends(get_todo_service),
],
page: int = Query(
default=1,
ge=1,
),
page_size: int = Query(
default=20,
ge=1,
le=100,
),
completed: bool | None = None,
):
offset = (page - 1) * page_size
items, total = await service.repository.list(
offset=offset,
limit=page_size,
completed=completed,
)
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size,
}
@router.get(
"/{todo_id}",
response_model=TodoResponse,
)
async def get_todo(
todo_id: int,
service: Annotated[
TodoService,
Depends(get_todo_service),
],
):
return await service.get(todo_id)
@router.patch(
"/{todo_id}",
response_model=TodoResponse,
)
async def update_todo(
todo_id: int,
payload: TodoUpdate,
service: Annotated[
TodoService,
Depends(get_todo_service),
],
):
values = payload.model_dump(
exclude_unset=True
)
return await service.update(
todo_id,
values,
)
@router.delete(
"/{todo_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_todo(
todo_id: int,
service: Annotated[
TodoService,
Depends(get_todo_service),
],
):
await service.delete(todo_id)
return Response(status_code=204)
这里的 list_todos 直接访问了 service.repository,演示上可以运行,但更推荐在服务层增加 list 方法,避免路由层穿透服务对象内部结构。
改进:
python
async def list(
self,
page: int,
page_size: int,
completed: bool | None,
):
offset = (page - 1) * page_size
items, total = await self.repository.list(
offset=offset,
limit=page_size,
completed=completed,
)
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size,
}
路由中改为:
python
return await service.list(
page=page,
page_size=page_size,
completed=completed,
)
11. 测试接口
安装测试依赖:
bash
python -m pip install pytest httpx
tests/test_todos.py:
python
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health():
response = client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "ok"
def test_create_todo():
response = client.post(
"/api/todos",
json={
"title": "学习 FastAPI",
"description": "完成第一个异步 API",
},
)
assert response.status_code == 201
body = response.json()
assert body["title"] == "学习 FastAPI"
assert body["completed"] is False
def test_invalid_todo():
response = client.post(
"/api/todos",
json={
"title": "",
},
)
assert response.status_code == 422
测试时不应该使用生产数据库。可以通过依赖覆盖替换数据库仓库:
python
app.dependency_overrides[
get_todo_repository
] = get_test_repository
这样测试可以使用临时数据库、内存数据库或 Mock 对象。
五、常见问题与实践建议
1. FastAPI 一定比 Flask 快吗
不能简单地这样比较。性能取决于:
- 请求类型;
- 同步或异步 IO;
- 数据库驱动;
- 序列化方式;
- 中间件;
- 服务器配置;
- worker 数量;
- 业务代码;
- 下游服务。
FastAPI 在类型校验、异步接口和自动文档方面体验很好,但如果接口主要被数据库慢查询限制,换框架未必明显提升性能。
2. async def 中可以调用同步库吗
可以调用,但会阻塞事件循环。例如:
python
@app.get("/bad")
async def bad():
result = requests.get(
"https://example.com"
)
return result.json()
requests 是同步 HTTP 客户端,在 async def 中直接调用会阻塞当前事件循环。
改进方式:
- 使用 httpx.AsyncClient;
- 使用异步数据库驱动;
- 把同步调用放到线程池;
- 将任务交给后台队列。
3. async def 是否适合 CPU 密集计算
不适合直接在事件循环中执行长时间 CPU 计算:
python
@app.get("/bad")
async def bad():
result = calculate_large_report()
return result
这会阻塞其他协程。可以:
- 使用进程池;
- 使用任务队列;
- 拆分计算;
- 使用专门的数据处理服务。
4. 422 和 400 应该如何区分
FastAPI 对请求参数和模型校验失败通常返回 422。它表示请求格式或数据结构不符合接口要求。
400 更适合:
- 业务层参数不合法;
- 请求无法被业务处理;
- 自定义协议错误。
项目中可以统一错误响应结构,但不要为了追求一个状态码而丢失错误语义。
5. response_model 为什么重要
如果直接返回数据库字典:
python
return user_dict
可能无意中返回:
- 密码哈希;
- 内部权限;
- 删除标记;
- 数据库字段;
- 内部调试信息。
response_model 可以明确允许返回哪些字段:
python
class UserResponse(BaseModel):
id: int
username: str
任何 API 都应该明确响应边界。
6. Pydantic 模型和 ORM 模型是否应该共用
不建议无条件共用。两者职责不同:
text
ORM 模型:
描述数据库表和持久化行为
Pydantic 模型:
描述 API 输入输出和校验
可以通过 from_attributes 或转换函数连接两者,但不要把数据库内部结构直接作为公共 API 契约。
7. FastAPI 依赖注入会不会变复杂
依赖注入在依赖较少时非常清晰,但项目变大后需要控制层级。建议:
- 每个依赖只负责一件事;
- 依赖命名清楚;
- 避免隐式修改全局状态;
- 数据库会话在请求范围内管理;
- 测试时支持 override;
- 高风险依赖记录日志。
8. 如何管理配置
使用环境变量和配置模型:
python
from pydantic_settings import (
BaseSettings,
)
class Settings(BaseSettings):
app_name: str = "Todo API"
database_url: str = "sqlite:///todo.db"
debug: bool = False
request_timeout: float = 5.0
class Config:
env_file = ".env"
settings = Settings()
安装配置依赖:
bash
python -m pip install pydantic-settings
不要把数据库密码、JWT 密钥和第三方 Token 写进代码。
9. FastAPI 如何处理认证
可以使用 OAuth2、JWT、API Key 等方式。简单的依赖示例:
python
from fastapi import Depends, HTTPException
from fastapi.security import (
HTTPBearer,
)
security = HTTPBearer()
async def get_current_user(
credentials=Depends(security),
):
token = credentials.credentials
user = verify_token(token)
if user is None:
raise HTTPException(
status_code=401,
detail="无效的认证凭证",
)
return user
路由中使用:
python
@app.get("/profile")
async def profile(
user=Depends(get_current_user),
):
return user
认证和授权仍然要分开,获取当前用户后,还要检查用户是否有访问具体资源的权限。
10. 如何处理后台任务
FastAPI 提供简单的 BackgroundTasks:
python
from fastapi import BackgroundTasks
def write_audit_log(user_id: int):
...
@app.post("/events")
async def create_event(
background_tasks: BackgroundTasks,
):
background_tasks.add_task(
write_audit_log,
1,
)
return {
"status": "accepted"
}
这种方式适合短小、非关键的后台动作。对于长时间任务、重试要求高的任务和跨实例任务,应使用 Celery、RQ、消息队列或持久化任务系统。
11. 如何部署多个 worker
开发环境:
bash
uvicorn app.main:app --reload
生产环境可以:
bash
uvicorn app.main:app \
--host 0.0.0.0 \
--port 8000 \
--workers 4
或使用 Gunicorn 管理 Uvicorn worker:
bash
gunicorn app.main:app \
-k uvicorn.workers.UvicornWorker \
--workers 4 \
--bind 0.0.0.0:8000
worker 数量要结合 CPU 和内存压测,不能盲目增加。
12. 异步数据库是否一定更好
异步数据库可以减少等待时阻塞事件循环,但也会增加:
- 异步编程复杂度;
- 连接池管理;
- 调试成本;
- 依赖兼容性;
- 事务边界理解难度。
如果服务流量不高、业务简单,成熟的同步数据库方案也可以满足需求。关键是让数据库访问模型与服务运行模型匹配。
六、进阶思考
1. Flask 与 FastAPI 的选择
| 维度 | Flask | FastAPI |
|---|---|---|
| 学习 HTTP 基础 | 直观 | 也较清晰 |
| 类型提示整合 | 需要额外组织 | 原生体验好 |
| 自动文档 | 需要扩展 | 内置支持 |
| 异步支持 | 可通过扩展和服务器实现 | 更自然 |
| 生态成熟度 | 历史久、生态广 | 现代 API 体验好 |
| 自由度 | 很高 | 约定更多 |
| 适合场景 | 轻量服务、传统同步应用 | API、异步 IO、数据服务 |
两者不是绝对替代关系。选择框架时应看团队经验、业务模型、依赖生态和部署环境。
2. ASGI 下的并发模型
一个事件循环可以管理多个协程:
text
请求 A 等待数据库
-> 切换处理请求 B
请求 B 等待 HTTP 服务
-> 切换处理请求 C
请求 C 计算完成
-> 返回响应
这要求所有等待型操作都具有异步特性。如果其中一个协程执行阻塞代码,整个事件循环都可能受影响。
因此异步项目的性能关键是:
- 识别阻塞调用;
- 选择正确的异步客户端;
- 设置连接和读取超时;
- 控制协程数量;
- 限制数据库连接;
- 对 CPU 任务隔离。
3. 数据库会话和连接池
生产服务不应为每个 SQL 临时创建底层连接。应该使用连接池:
text
应用启动
-> 创建数据库连接池
请求到达
-> 从连接池获取连接
-> 执行事务
-> 归还连接
应用关闭
-> 关闭连接池
连接池参数需要考虑:
- 最大连接数;
- 最小连接数;
- 获取连接超时;
- 空闲超时;
- 连接回收;
- 数据库最大连接;
- worker 数量。
多个 worker 进程会分别创建连接池,所以总连接数需要按进程计算。
4. 统一异常模型
可以定义业务异常:
python
class AppError(Exception):
def __init__(
self,
code: str,
message: str,
status_code: int = 400,
):
self.code = code
self.message = message
self.status_code = status_code
统一异常处理:
python
from fastapi import Request
from fastapi.responses import JSONResponse
@app.exception_handler(AppError)
async def handle_app_error(
request: Request,
exc: AppError,
):
return JSONResponse(
status_code=exc.status_code,
content={
"code": exc.code,
"message": exc.message,
},
)
统一格式有利于前端处理、日志统计和接口文档。
5. 中间件和依赖如何选择
适合中间件:
- 所有请求都需要的逻辑;
- 请求 ID;
- 统一耗时;
- CORS;
- 全局日志;
- 全局异常记录。
适合依赖:
- 当前用户;
- 当前数据库会话;
- 某个路由组的权限;
- 分页和筛选参数;
- 可复用的业务前置检查。
不要把所有逻辑都写进中间件,否则业务边界会变得不清晰。
6. API 版本和兼容性
当 API 对外提供服务时,应考虑版本:
text
/api/v1/todos
/api/v2/todos
接口升级时要保持:
- 旧客户端短期可用;
- 字段新增向后兼容;
- 删除字段有迁移周期;
- 错误码有文档;
- OpenAPI 文档同步;
- 监控区分不同版本。
7. 流式响应
FastAPI 适合处理流式响应:
python
from fastapi.responses import (
StreamingResponse,
)
async def event_stream():
for index in range(3):
yield f"data: chunk-{index}\n\n"
@app.get("/stream")
async def stream():
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
)
流式接口需要考虑:
- 客户端断开;
- 连接超时;
- 心跳;
- 资源释放;
- 代理缓冲;
- 并发连接;
- 取消任务。
8. 可观测性
建议为 FastAPI 服务记录:
text
请求量
状态码分布
P50、P95、P99
请求体大小
响应体大小
数据库耗时
外部服务耗时
连接池使用
事件循环阻塞
异常类型
用户和租户维度
每个请求都应带 request_id 或 trace_id:
python
import uuid
@app.middleware("http")
async def add_request_id(
request,
call_next,
):
request_id = request.headers.get(
"X-Request-Id"
) or str(uuid.uuid4())
response = await call_next(request)
response.headers[
"X-Request-Id"
] = request_id
return response
9. 容器化部署
Dockerfile 示例:
dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
生产环境还需要考虑:
- 非 root 用户;
- 健康检查;
- 资源限制;
- 日志输出到标准输出;
- 配置通过环境变量注入;
- 优雅关闭;
- 多副本;
- 反向代理;
- 数据库迁移。
结论
FastAPI 将 Python 类型提示、Pydantic 数据模型、ASGI 异步能力、依赖注入和 OpenAPI 文档结合起来,为构建现代 Web API 提供了比较完整的开发体验。
本文通过待办事项服务介绍了:
- FastAPI 应用和路由;
- 路径参数和查询参数;
- Pydantic 请求与响应模型;
- 依赖注入;
- 自动 API 文档;
- SQLite 异步访问;
- 项目分层;
- 接口测试;
- 同步与异步代码;
- 部署和可观测性。
学习 FastAPI 时,建议按照以下路径继续:
text
路由和参数
-> Pydantic 模型
-> 依赖注入
-> 数据库会话
-> 认证授权
-> 异步客户端
-> 后台任务
-> 流式响应
-> 容器化和监控
FastAPI 的优势不只在于代码简洁,更在于它把 API 的输入、输出和依赖关系显式表达出来。当项目逐步变大时,这些显式约束能够减少参数错误、文档滞后和接口边界混乱。
下一篇可以继续学习《FastAPI 的依赖注入、参数校验与接口文档》,深入分析 Depends、Pydantic 校验模型、OpenAPI Schema 和复杂依赖组合。