开飞机的舒克 halo.gaodaimou.cn
如果你已经写过 Flask 或 Django,再来看 FastAPI,第一感觉通常是"接口代码终于和接口契约长在一起了"。我们写下的类型标注、Pydantic 模型、依赖函数,不只是给人看的注释,它们会直接驱动参数校验、数据序列化、OpenAPI 文档和交互式调试页面。
引言:为什么我会推荐 FastAPI
在 Python 后端项目里,我们经常重复处理几类问题:解析请求参数、校验请求体、生成接口文档、处理登录态、管理数据库连接、统一异常响应。传统写法当然能做,但业务一复杂,这些"周边代码"很容易淹没真正的业务逻辑。
FastAPI 的优势在于,它把这些高频能力做成了框架默认能力:
| 核心能力 | 解决的问题 | 实战价值 |
|---|---|---|
| 类型标注 + Pydantic | 手写参数解析和校验很繁琐 | 请求数据自动转换、自动校验、自动提示 |
| 自动文档 | 文档容易滞后于代码 | /docs 和 /redoc 随代码同步生成 |
| 异步支持 | I/O 等待拖慢吞吐 | 适合外部 API、数据库、文件等 I/O 密集场景 |
| 依赖注入 | 公共逻辑散落在接口里 | 认证、权限、分页、Session 统一复用 |
| OpenAPI 标准 | 前后端协作和 SDK 生成困难 | 更容易和测试、网关、客户端工具集成 |
我个人最喜欢 FastAPI 的地方,是它能逼着我们把"接口边界"写清楚。比如你在 main.py 里写的 ProductCreate、UserOut,在 testCrud.py 里写的 Person,以及 Depends.py 里抽出的分页、用户、权限依赖,本质上都在做同一件事:让接口函数更专注于业务,而不是到处处理样板逻辑。
快速上手:第一个可运行接口
先安装依赖:
arduino
pip install fastapi "uvicorn[standard]"
创建 app.py:
python
from fastapi import FastAPI
app = FastAPI(title="FastAPI Demo")
@app.get("/")
def read_root() -> dict[str, str]:
# 返回值会被 FastAPI 自动序列化为 JSON
return {"message": "Hello FastAPI"}
@app.get("/users/{user_id}")
def get_user(user_id: int, keyword: str | None = None) -> dict[str, int | str | None]:
# user_id 来自路径参数,keyword 来自查询参数
return {
"user_id": user_id,
"keyword": keyword,
}
启动服务:
lua
uvicorn app:app --reload
访问:
http://127.0.0.1:8000/http://127.0.0.1:8000/users/1?keyword=alicehttp://127.0.0.1:8000/docs
这里最值得注意的是:我们没有额外写 Swagger 配置,FastAPI 会从路由、函数签名和类型标注里推导出接口信息。你写代码时顺手加上的 int、str | None,已经变成了运行时校验和文档的一部分。
路由与参数:路径参数、查询参数和显式校验
在你的 main.py 中,已经有路径参数和查询参数示例。我们可以把它整理成一个更贴近实战的版本:
python
from fastapi import FastAPI, Path, Query
app = FastAPI(title="Route Params Demo")
@app.get("/users/{user_id}")
def get_user(
user_id: int = Path(ge=1, description="用户 ID,必须大于等于 1"),
keyword: str | None = Query(default=None, min_length=2, max_length=30),
) -> dict[str, int | str | None]:
# 能执行到这里,说明 user_id 和 keyword 已经通过校验
return {
"user_id": user_id,
"keyword": keyword,
}
常用校验参数可以这样记:
| 参数 | 含义 | 示例 |
|---|---|---|
ge |
大于等于 | Path(ge=1) |
gt |
大于 | Field(gt=0) |
le |
小于等于 | Query(le=100) |
lt |
小于 | Field(lt=10) |
min_length |
最小长度 | Query(min_length=2) |
max_length |
最大长度 | Field(max_length=50) |
pattern |
正则匹配 | Query(pattern="^a") |
我的习惯是:凡是能在接口边界校验的规则,就尽量在
Path、Query、Field里写清楚。这样错误会更早暴露,业务代码也会更干净。
请求体验证:用 Pydantic 管住输入边界
真实业务里,请求体通常不是一个字段,而是一组有约束的数据。比如创建商品时,name 不能为空,price 必须大于 0,库存不能为负数。这类规则非常适合交给 Pydantic 模型。
python
from fastapi import FastAPI, status
from pydantic import BaseModel, Field
app = FastAPI(title="Request Body Demo")
class ProductCreate(BaseModel):
name: str = Field(min_length=2, max_length=50)
price: float = Field(gt=0)
stock: int = Field(default=0, ge=0)
description: str | None = None
@app.post("/products", status_code=status.HTTP_201_CREATED)
def create_product(product: ProductCreate) -> dict[str, ProductCreate | str]:
# product 是已经完成校验和类型转换的 Python 对象
return {
"message": "商品创建成功",
"product": product,
}
如果客户端传入 price=-1,FastAPI 会直接返回 422 Unprocessable Entity,并说明具体哪个字段不合法。这个体验对前后端联调非常友好,因为错误不再是模糊的"参数错误",而是结构化、可定位的校验结果。
还可以用到 response_model:
python
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(title="Response Model Demo")
class UserIn(BaseModel):
username: str
password: str
class UserOut(BaseModel):
id: int
username: str
@app.post("/register", response_model=UserOut)
def register(user: UserIn) -> dict[str, int | str]:
# 即使这里返回了 password,FastAPI 也会按 response_model 过滤输出
return {
"id": 1,
"username": user.username,
"password": user.password,
}
这在生产项目里很重要:输入模型和输出模型最好分开。用户注册时可以接收密码,但接口响应绝不应该把密码返回给客户端。
自动文档:让接口文档跟着代码走
FastAPI 默认会生成两套文档页面:
/docs:Swagger UI,适合直接在浏览器里调试接口。/redoc:ReDoc,适合阅读结构化接口文档。
自动文档不是"好看"的附属品,而是实实在在提升协作效率的工具。前端同学可以直接看到参数类型、是否必填、响应结构;测试同学可以直接发请求;后端自己也能快速验证接口行为。
建议你在写路由时顺手补充这些信息:
python
from fastapi import FastAPI, Query
app = FastAPI(
title="Person API",
description="人员管理示例接口",
version="1.0.0",
)
@app.get("/people")
def list_people(
page: int = Query(default=1, ge=1, description="页码,从 1 开始"),
size: int = Query(default=10, ge=1, le=100, description="每页数量,最大 100"),
) -> dict[str, int | list[dict[str, int | str]]]:
# 文档里会展示 page 和 size 的默认值、范围和说明
return {
"page": page,
"size": size,
"data": [{"id": 1, "name": "Alice", "age": 30}],
}
写项目时我常用一个小原则:接口字段可以少,但含义不能含糊 。把 description 写在参数旁边,比单独维护一份容易过期的文档更可靠。
异步支持:什么时候用 async def
FastAPI 同时支持普通的 def 和异步的 async def。这对迁移老项目很友好:同步数据库驱动、同步 SDK 可以继续用 def;异步 HTTP 客户端、异步数据库驱动则适合放在 async def 里。
python
import httpx
from fastapi import FastAPI
app = FastAPI(title="Async Demo")
@app.get("/github/{username}")
async def get_github_user(username: str) -> dict:
# httpx.AsyncClient 支持 await,适合在 async def 接口中使用
async with httpx.AsyncClient(timeout=5) as client:
response = await client.get(f"https://api.github.com/users/{username}")
response.raise_for_status()
return response.json()
异步的价值不是让 CPU 计算突然变快,而是在等待网络、数据库、文件系统等 I/O 时释放执行权,让服务能处理更多请求。简单判断原则如下:
- 调用的库需要
await:接口写async def。 - 调用的是同步库:接口写普通
def。 - 不确定时:先用
def,等引入异步依赖时再调整。
需要注意的是,不要在 async def 里直接执行重型阻塞代码。比如同步大文件处理、复杂 CPU 计算、阻塞式第三方 SDK,都可能拖住事件循环。遇到这种场景,通常要考虑线程池、任务队列或独立计算服务。
依赖注入:把公共逻辑从接口里拿出去
你提供的 Depends.py 是很典型的依赖注入学习材料:分页、当前用户、管理员权限、数据库连接,都可以抽成依赖函数。FastAPI 会在执行接口前自动解析这些依赖,并把返回值传给接口参数。
python
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException, Query, status
from pydantic import BaseModel
app = FastAPI(title="Dependency Demo")
class Pagination(BaseModel):
page: int
size: int
offset: int
def get_pagination(
page: int = Query(default=1, ge=1),
size: int = Query(default=10, ge=1, le=100),
) -> Pagination:
# 分页规则只写一次,多个列表接口都能复用
return Pagination(page=page, size=size, offset=(page - 1) * size)
def get_current_user(authorization: Annotated[str | None, Header()] = None) -> dict:
# 示例中用硬编码 token,真实项目应查询缓存、数据库或认证服务
users = {
"user-token": {"id": 1, "name": "alice", "role": "user"},
"admin-token": {"id": 2, "name": "boss", "role": "admin"},
}
if authorization not in users:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效 token")
return users[authorization]
@app.get("/orders")
def list_orders(
pagination: Annotated[Pagination, Depends(get_pagination)],
current_user: Annotated[dict, Depends(get_current_user)],
) -> dict:
# 接口不关心分页和认证怎么来的,只专注业务返回
return {
"user": current_user["name"],
"page": pagination.page,
"size": pagination.size,
"data": [{"id": 1, "title": "order-1"}],
}
依赖注入最容易被低估。刚入门时,你可能觉得它只是"少写几行参数解析";项目变大后,你会发现它更大的价值是统一业务入口的上下文:当前用户是谁、有没有权限、数据库 Session 如何创建和释放、分页规则是否一致,都可以放在依赖里管理。
依赖也可以继续依赖其他依赖。比如后台接口常见的管理员校验:
python
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException, status
app = FastAPI(title="Admin Dependency Demo")
def get_current_user(authorization: Annotated[str | None, Header()] = None) -> dict:
users = {
"user-token": {"id": 1, "name": "alice", "role": "user"},
"admin-token": {"id": 2, "name": "boss", "role": "admin"},
}
if authorization not in users:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效 token")
return users[authorization]
def require_admin(
current_user: Annotated[dict, Depends(get_current_user)],
) -> dict:
# 先获取当前用户,再判断角色
if current_user["role"] != "admin":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="需要管理员权限")
return current_user
@app.get("/admin/dashboard")
def admin_dashboard(admin_user: Annotated[dict, Depends(require_admin)]) -> dict[str, str]:
return {
"message": "欢迎进入管理后台",
"admin": admin_user["name"],
}
这种分层依赖在会员权限、租户隔离、后台管理、对象级授权里都非常好用。
CRUD 示例:从内存列表理解接口组织
用内存列表写了人员增删改查,这很适合入门阶段理解 REST 风格接口。我们可以整理成下面这个更紧凑的版本:
python
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
app = FastAPI(title="Person CRUD Demo")
people = [
{"id": 1, "name": "Alice", "age": 30},
{"id": 2, "name": "Bob", "age": 25},
]
class Person(BaseModel):
id: int = Field(ge=1)
name: str = Field(min_length=1, max_length=30)
age: int = Field(ge=0, le=150)
@app.get("/people")
def list_people() -> list[dict[str, int | str]]:
return people
@app.get("/people/{person_id}")
def get_person(person_id: int) -> dict[str, int | str]:
person = next((item for item in people if item["id"] == person_id), None)
if person is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="人员不存在")
return person
@app.post("/people", status_code=status.HTTP_201_CREATED)
def add_person(person: Person) -> dict[str, str | list[dict[str, int | str]]]:
if any(item["id"] == person.id for item in people):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="人员 ID 已存在")
people.append(person.model_dump())
return {"message": "添加成功", "data": people}
入门示例用内存列表没问题,但生产项目要注意:
- 内存数据会随着进程重启丢失。
- 多进程部署时,每个进程都有自己的内存副本,数据会不一致。
- 真正项目应接入数据库,并把数据访问逻辑放到 service/repository 层。
异常处理:让错误响应保持一致
FastAPI 内置的 HTTPException 已经足够应对大部分接口错误。比如资源不存在返回 404,权限不足返回 403,参数语义不合法返回 400。
python
from fastapi import FastAPI, HTTPException, status
app = FastAPI(title="Error Demo")
users = {
1: {"id": 1, "name": "Alice"},
2: {"id": 2, "name": "Bob"},
}
@app.get("/users/{user_id}")
def get_user(user_id: int) -> dict[str, int | str]:
user = users.get(user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
return user
如果项目希望统一业务错误格式,可以定义自定义异常和异常处理器:
python
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
app = FastAPI(title="Business Error Demo")
class BusinessException(Exception):
def __init__(self, code: int, message: str):
self.code = code
self.message = message
@app.exception_handler(BusinessException)
async def business_exception_handler(request: Request, exc: BusinessException):
# request 参数可用于读取 header、path、日志追踪 ID 等上下文
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"code": exc.code, "message": exc.message},
)
@app.post("/orders")
def create_order(item_id: int) -> dict[str, int]:
if item_id <= 0:
raise BusinessException(code=40001, message="物品 ID 无效")
return {"order_id": 1}
我建议团队尽早约定错误响应格式,比如统一使用 code、message、data。否则项目做大之后,前端会被各种形状的错误响应折磨。
数据库 Session:依赖注入的典型生产用法
FastAPI 不绑定具体 ORM。你可以使用 SQLAlchemy、SQLModel、Tortoise ORM,甚至直接写 SQL。核心思路是:把数据库 Session 的创建和关闭放进依赖里。
python
from typing import Annotated
from fastapi import Depends, FastAPI
from sqlalchemy import Column, Integer, String, create_engine, select
from sqlalchemy.orm import Session, declarative_base, sessionmaker
DATABASE_URL = "sqlite:///./demo.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(50), nullable=False)
Base.metadata.create_all(bind=engine)
app = FastAPI(title="Database Dependency Demo")
def get_db():
db = SessionLocal()
try:
yield db
finally:
# 请求结束后关闭连接,避免资源泄漏
db.close()
DbSession = Annotated[Session, Depends(get_db)]
@app.post("/db/users")
def create_user(name: str, db: DbSession) -> dict[str, int | str]:
user = User(name=name)
db.add(user)
db.commit()
db.refresh(user)
return {"id": user.id, "name": user.name}
@app.get("/db/users")
def list_users(db: DbSession) -> list[dict[str, int | str]]:
users = db.execute(select(User)).scalars().all()
return [{"id": user.id, "name": user.name} for user in users]
yield 依赖是 FastAPI 里非常实用的模式:yield 前创建资源,yield 后释放资源。数据库连接、事务上下文、临时文件、外部客户端连接,都可以用类似方式管理。
最佳实践:少踩坑的写法
- 优先写清类型标注
路径参数、查询参数、请求体、响应模型都尽量明确。类型写得越准,校验、文档和编辑器提示越好。 - 输入模型和输出模型分离
例如UserIn接收密码,UserOut不返回密码。不要把数据库模型直接暴露给外部接口。 - 公共逻辑放进依赖
分页、认证、权限、数据库 Session、租户信息都适合用Depends复用。 - 同步库配
def,异步库配async def
不要为了"看起来高级"盲目使用 async。异步要和真正支持await的库配套使用。 - 统一异常结构
业务异常、权限异常、资源不存在都应该有稳定的响应格式,方便前端和测试处理。 - 项目变大后拆分文件
不要把所有接口都塞进main.py。推荐按模块拆成routers/、schemas/、services/、dependencies/、database.py。 - 用测试覆盖依赖边界
FastAPI 支持 dependency override,测试时可以替换数据库、当前用户或外部服务,非常适合写单元测试和接口测试。
一个常见的目录结构可以这样组织:
css
app/
main.py
database.py
dependencies.py
routers/
users.py
orders.py
schemas/
user.py
order.py
services/
user_service.py
order_service.py
tests/
test_users.py
小结
FastAPI 的魅力不只在"快",更在于它把现代 Python 后端开发里几件高频事情做成了默认能力:类型即契约、校验即文档、依赖即复用。
从你现有的示例来看,路由、参数校验、请求体模型、响应过滤、异常处理、CRUD 和依赖注入都已经覆盖到了。下一步如果继续深入,我会建议你把这些示例拆成多文件结构,再接入真实数据库和测试用例。这样项目会从"能跑的示例"逐渐变成"接近生产的服务骨架"。
如果你刚开始学 FastAPI,可以按这个顺序练习:
- 先写基础路由和路径参数。
- 再用 Pydantic 管理请求体。
- 接着给接口加
response_model和状态码。 - 然后抽取分页、登录态、权限等依赖。
- 最后接入数据库,并为核心接口补测试。
走完这一轮,你会发现 FastAPI 的核心思路其实很一致:把边界写清楚,把重复逻辑抽出去,让接口函数只表达业务本身。