FastAPI:python http框架小钢炮
虚拟环境
从 Python 3.3 开始,venv 就是标准库的一部分。
你不需要安装任何第三方工具,只要有 Python 就能创建虚拟环境。
venv 做的事情很纯粹——创建一个独立的 Python 环境,复制必要的二进制文件和库。它不做任何"魔法",创建出来的环境结构清晰,你可以随时查看 .venv 目录里到底有什么。
在 Windows、macOS、Linux 上,venv 的行为和命令完全一致,只是激活脚本的路径略有不同(Windows 是 .venv\Scripts\activate,Unix 系是 .venv/bin/activate)。这让跨平台开发变得简单。
✅ venv + requirements.txt 这是 Python 社区最经典、最成熟的依赖管理方式:
# 创建虚拟环境
python -m venv .venv
# 生成 【依赖列表】
pip freeze > requirements.txt
# 根据 【依赖列表】安装
pip install -r requirements.txt
# 根据 【依赖列表】安装,使用国内镜像源
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 安装三方库
pip install 三方库1 三方库2 -i https://pypi.tuna.tsinghua.edu.cn/simple
前言
FastAPI 是基于 Python 类型提示构建的高性能 Web 框架,原生支持异步、自动生成接口文档、内置参数校验与数据序列化能力,是当前 Python 后端生态中工程化效率最高的框架之一。
本文从基础环境搭建到完整项目实战,覆盖路由管理、参数解析、数据校验、异常处理、统一响应等核心工程能力,所有代码均可直接复制运行。
一、安装
1.1 环境安装
FastAPI 运行依赖 ASGI 服务器,官方推荐使用 uvicorn。执行以下命令安装核心依赖:
pip install fastapi uvicorn
1.2 最小可运行示例
创建 main.py 文件,编写最简 FastAPI 应用:
# main.py
from fastapi import FastAPI
# 创建 FastAPI 应用实例,可配置标题、描述等文档信息
wtt_app = FastAPI(
title="FastAPI 入门示例",
description="第一个 FastAPI 程序,演示基础能力",
version="1.0.0"
)
# 装饰器注册 GET 路由,路径为根路径
@wtt_app.get("/")
def root():
return {"message": "Hello FastAPI!"}
1.3 服务启动方式
在终端执行以下命令启动服务,--reload 参数表示代码修改后自动重启,仅开发环境使用:
uvicorn main:wtt_app --reload
启动成功后访问 http://127.0.0.1:8000 即可看到返回结果。
1.4 自动生成接口文档
FastAPI 基于 OpenAPI 规范自动生成两份接口文档,启动服务后直接访问:
- Swagger UI 交互式文档:
http://127.0.0.1:8000/docs(可在线调试接口) - ReDoc 文档:
http://127.0.0.1:8000/redoc(适合阅读的静态文档)
二、路由路径注册与模块化管理
2.1 基础路由与路径参数
通过路径中 {变量名} 的方式定义路径参数,配合 Python 类型注解可自动完成类型转换与校验:
# 追加到 main.py 中
@app.get("/items/{item_id}", summary="获取商品详情")
def get_item(item_id: int):
"""
- item_id: 路径参数, 用于 用 【 :int 】 进行修饰,所以 必须为整数,FastAPI 会自动做类型校验
"""
return {"item_id": item_id, "name": f"商品{item_id}"}
测试命令:
curl http://127.0.0.1:8000/items/100
若传入非数字参数,FastAPI 会自动返回 422 参数校验错误。
2.2 路由层级与请求方法
FastAPI 支持所有标准 HTTP 方法(GET/POST/PUT/DELETE 等),同一路径可绑定不同请求方法:
# 追加到 main.py 中
@app.post("/items", summary="创建商品")
def create_item():
return {"message": "商品创建成功"}
@app.put("/items/{item_id}", summary="更新商品")
def update_item(item_id: int):
return {"item_id": item_id, "message": "商品更新成功"}
2.3 APIRouter 路由组 管理
大型项目中不能将所有路由写在同一个文件中,FastAPI 提供 APIRouter 实现路由模块化拆分,支持路由前缀、标签统一配置。
第一步:创建用户路由模块 routers/user.py
# routers/user.py
from fastapi import APIRouter
# 创建路由实例
# prefix: 路由统一前缀,该模块下所有路由自动添加 /user 前缀
# tags: 接口文档分组标签
user_router = APIRouter(prefix="/user", tags=["用户管理"])
@user_router.get("/{user_id}", summary="获取用户信息")
def get_user(user_id: int):
return {"user_id": user_id, "username": f"用户{user_id}"}
@user_router.post("/add", summary="新增用户")
def add_user():
return {"message": "用户创建成功"}
第二步:主应用挂载路由
# main.py 中追加导入与挂载
from routers.user import user_router
# 挂载用户路由模块
app.include_router(user_router)
测试命令:
curl http://127.0.0.1:8000/user/1
访问文档 /docs 可看到接口已按「用户管理」分组展示。
三、路由函数参数解析(GET/POST 全场景)
3.1 GET 请求参数解析
GET 请求参数分为两类:路径参数(嵌入 URL)、查询参数(URL 中 ? 后面的键值对)。
3.1.1 路径参数 + 查询参数组合
# 追加到 routers/user.py 中
@user_router.get("/list", summary="获取用户列表")
def get_user_list(
page: int = 1, # 查询参数,带默认值
page_size: int = 10
):
"""
page、page_size 均为查询参数,未传时使用默认值
"""
return {
"page": page,
"page_size": page_size,
"list": [{"id": i, "name": f"用户{i}"} for i in range(page_size)]
}
测试命令:
# 不传参,使用默认值
curl http://127.0.0.1:8000/user/list
# 传参覆盖默认值
curl "http://127.0.0.1:8000/user/list?page=2&page_size=5"
3.1.2 可选查询参数
通过 Optional 或 类型 | None 声明可选参数,未传时值为 None:
from typing import Optional
@user_router.get("/search", summary="搜索用户")
def search_user(
keyword: str,
status: Optional[int] = None # 可选参数
):
return {"keyword": keyword, "status": status}
3.2 POST 请求参数解析
3.2.1 基础请求体(Pydantic 模型)
POST 请求体通过 Pydantic 的 BaseModel 定义,FastAPI 自动解析 JSON 请求体并做类型校验。
from pydantic import BaseModel
# 定义请求体模型
class UserCreate(BaseModel):
username: str
age: int
email: str | None = None # 可选字段, 语法解释 { : str | None 是 类型注解 } 和 { = None 默认值 }
@user_router.post("/create", summary="创建用户(请求体方式)")
def create_user(user: UserCreate):
# 直接通过属性访问参数
return {
"username": user.username,
"age": user.age,
"email": user.email
}
测试命令:
curl -X POST http://127.0.0.1:8000/user/create \
-H "Content-Type: application/json" \
-d '{"username":"zhangsan","age":20,"email":"zhangsan@example.com"}'
3.2.2 嵌套请求模型 (类 的 组合模式)
Pydantic 支持模型嵌套,适合复杂结构的请求体:
class Address(BaseModel):
province: str
city: str
detail: str
class UserWithAddress(BaseModel):
username: str
age: int
address: Address # 嵌套模型
@user_router.post("/create_with_address", summary="创建用户(带地址)")
def create_user_with_address(user: UserWithAddress):
return {
"username": user.username,
"address": user.address.city
}
3.2.3 表单数据解析
解析 application/x-www-form-urlencoded 格式的表单数据,需先安装依赖:
pip install python-multipart
代码示例:
from fastapi import Form
@user_router.post("/login", summary="表单登录")
def login(
username: str = Form(..., description="用户名"),
password: str = Form(..., description="密码")
):
return {"username": username, "login_status": True}
测试命令:
curl -X POST http://127.0.0.1:8000/user/login \
-d "username=admin&password=123456"
3.2.4 文件上传接收
FastAPI 支持单文件、多文件上传,通过 UploadFile 接收文件对象:
from fastapi import File, UploadFile
@user_router.post("/upload_avatar", summary="上传头像")
async def upload_avatar(file: UploadFile = File(..., description="头像文件")):
# 读取文件内容(异步读取)
content = await file.read()
return {
"filename": file.filename,
"content_type": file.content_type,
"file_size": len(content)
}
测试命令:
curl -X POST http://127.0.0.1:8000/user/upload_avatar \
-F "file=@avatar.png"
四、前端参数校验:原生能力全解
FastAPI 结合 Pydantic 实现零额外依赖的参数校验,覆盖基础约束到自定义规则全场景。
4.1 基础校验规则
4.1.1 路径/查询参数校验
通过 Path、Query 类配置校验规则,支持数值范围、字符串长度等约束:
from fastapi import Path, Query
@user_router.get("/check/{user_id}", summary="参数校验示例")
def check_param(
# 路径参数校验:必须大于0
user_id: int = Path(..., gt=0, description="用户ID,必须大于0"),
# 查询参数校验:字符串长度3-20
username: str = Query(..., min_length=3, max_length=20, description="用户名")
):
return {"user_id": user_id, "username": username}
4.1.2 请求体模型校验
通过 Field 为模型字段配置校验规则,是业务开发中最常用的方式:
from pydantic import BaseModel, Field
class UserCreateV2(BaseModel):
username: str = Field(min_length=3, max_length=20, description="用户名,3-20字符")
age: int = Field(ge=0, le=120, default=18, description="年龄,0-120岁")
phone: str = Field(pattern=r"^1[3-9]\d{9}$", description="手机号格式校验")
email: str | None = Field(default=None, description="可选邮箱")
4.1.3 枚举值校验
使用 Python 标准库 Enum 限制参数只能为指定枚举值:
from enum import Enum
class UserStatus(str, Enum):
NORMAL = "normal"
DISABLED = "disabled"
VIP = "vip"
@user_router.get("/status/{status}", summary="枚举值校验")
def get_user_by_status(status: UserStatus):
return {"status": status.value, "message": f"查询{status.value}状态用户"}
4.2 进阶校验能力
4.2.1 自定义字段校验函数
通过 Pydantic 的 @field_validator 装饰器实现复杂自定义校验逻辑:
from pydantic import field_validator
class UserRegister(BaseModel):
username: str = Field(min_length=3, max_length=20)
password: str = Field(min_length=6, max_length=32)
@field_validator("password")
@classmethod
def validate_password(cls, value: str) -> str:
# 自定义规则:密码必须包含至少一位数字
if not any(char.isdigit() for char in value):
raise ValueError("密码必须包含至少一位数字")
return value
校验失败时会自动返回 422 错误,错误信息包含自定义提示。
4.2.2 校验失败默认响应格式
参数校验失败时,FastAPI 默认返回如下结构的 422 响应:
{
"detail": [
{
"loc": ["body", "password"],
"msg": "密码必须包含至少一位数字",
"type": "value_error"
}
]
}
后续全局异常处理章节会讲解如何将其封装为统一业务格式。
五、全局异常处理:统一错误响应
5.1 内置 HTTPException 使用
业务逻辑中可直接抛出 HTTPException 终止请求,返回指定状态码和错误信息:
from fastapi import HTTPException
@user_router.get("/{user_id}/info", summary="查询用户(演示异常抛出)")
def get_user_info(user_id: int):
if user_id <= 0:
# 抛出HTTP异常,指定状态码和错误详情
raise HTTPException(status_code=400, detail="用户ID不合法")
if user_id > 1000:
raise HTTPException(status_code=404, detail="用户不存在")
return {"user_id": user_id, "username": "test_user"}
5.2 全局异常处理器注册
通过 @app.exception_handler() 装饰器注册异常处理器,可拦截指定类型的异常并自定义返回格式。
5.3 自定义业务异常类
实际项目中通常定义业务异常,区分系统错误与业务错误:
# common/exceptions.py
class BizException(Exception):
"""自定义业务异常类"""
def __init__(self, code: int, message: str):
self.code = code
self.message = message
super().__init__(message)
5.4 常见异常统一封装
在主应用中注册全局异常处理器,覆盖业务异常、参数校验异常、404、500 等场景:
# main.py 追加内容
from fastapi import Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from common.exceptions import BizException
# 1. 捕获自定义业务异常
@app.exception_handler(BizException)
async def biz_exception_handler(request: Request, exc: BizException):
return JSONResponse(
status_code=200, # 业务异常HTTP状态码统一200,通过业务code区分
content={
"code": exc.code,
"message": exc.message,
"data": None
}
)
# 2. 捕获参数校验异常
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
# 提取第一个错误信息
first_error = exc.errors()[0]
error_msg = f"参数[{'.'.join(map(str, first_error['loc']))}]:{first_error['msg']}"
return JSONResponse(
status_code=400,
content={
"code": 400,
"message": error_msg,
"data": None
}
)
# 3. 捕获HTTP异常(404、405等)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"code": exc.status_code,
"message": exc.detail,
"data": None
}
)
# 4. 捕获服务器内部异常(500)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
# 生产环境可在此处记录错误日志
return JSONResponse(
status_code=500,
content={
"code": 500,
"message": "服务器内部错误",
"data": None
}
)
业务异常使用示例:
@user_router.get("/biz_test/{user_id}", summary="演示业务异常")
def biz_test(user_id: int):
if user_id == 0:
# 抛出自定义业务异常
raise BizException(code=1001, message="用户ID不能为0")
return {"user_id": user_id}
六、统一数据格式返回:标准化响应
6.1 通用响应结构设计
前后端分离项目通常采用 code + message + data 三段式统一响应结构,便于前端统一处理。
6.2 统一响应封装工具
创建通用响应模型与工具函数,所有接口统一返回该结构:
# common/response.py
from pydantic import BaseModel
from typing import Generic, TypeVar, Optional
# 泛型,支持不同类型的data字段
T = TypeVar("T")
class ResponseResult(BaseModel, Generic[T]):
"""统一响应模型"""
code: int = 200
message: str = "success"
data: Optional[T] = None
def success(data: T = None, message: str = "操作成功") -> ResponseResult[T]:
"""成功响应封装"""
return ResponseResult(code=200, message=message, data=data)
def fail(code: int = 400, message: str = "操作失败") -> ResponseResult:
"""失败响应封装"""
return ResponseResult(code=code, message=message, data=None)
6.3 Response Model 响应模型
通过 response_model 参数指定接口返回模型,可自动过滤多余字段,保证数据安全:
from common.response import ResponseResult, success
# 定义用户响应模型(不返回敏感字段)
class UserResp(BaseModel):
user_id: int
username: str
email: str | None
@user_router.get("/resp/{user_id}", summary="统一响应格式示例", response_model=ResponseResult[UserResp])
def get_user_resp(user_id: int):
# 模拟数据库返回的完整数据(含密码等敏感字段)
user_data = {
"user_id": user_id,
"username": "zhangsan",
"password": "123456", # 该字段会被自动过滤
"email": "zhangsan@example.com"
}
return success(data=user_data)
接口返回结果中不会包含 password 字段,保证数据安全。
七、中间件:请求全链路全局管控
中间件(Middleware)是 FastAPI 中拦截 HTTP 请求与响应的全局钩子,运行在「客户端请求」和「路由函数」之间,遵循经典的洋葱模型:请求进来时逐层穿透中间件,响应返回时再逐层反向穿透。
它适合处理所有接口都需要的通用逻辑,比如:请求日志、跨域处理、接口耗时统计、全局鉴权、响应头统一添加等,避免在每个路由函数中重复编写代码。
7.1 中间件基础语法
FastAPI 提供两种使用中间件的方式:自定义中间件(用装饰器定义)和内置/第三方中间件(用 add_middleware 注册)。
7.1.1 自定义中间件标准结构
使用 @app.middleware("http") 装饰器定义全局 HTTP 中间件,固定结构如下:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import time
app = FastAPI()
@app.middleware("http")
async def custom_middleware(request: Request, call_next):
# ========== 请求阶段:路由执行前执行 ==========
# 在这里做请求预处理:日志、鉴权、参数校验等
start_time = time.time()
# 调用 call_next 将请求传递给下一个中间件/路由函数
# 它会返回最终的响应对象
response = await call_next(request)
# ========== 响应阶段:路由执行后执行 ==========
# 在这里做响应后处理:添加响应头、统计耗时、日志收尾等
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
# 必须返回响应对象
return response
核心说明:
- @app.middleware(“http”) 里的
"http"目前是固定的唯一合法写法,不能随意替换为其他字符串。 request: Request:封装了完整的请求信息(路径、方法、请求头、参数等)call_next:一个可调用对象,调用它才会将请求向下传递;不调用则请求直接终止- 中间件必须是异步函数(
async def),call_next必须用await调用 - 最终必须返回响应对象
7.2 高频实战场景
7.2.1 场景一:接口耗时统计
这是最常用的中间件场景,自动为所有接口计算执行耗时并写入响应头。
from fastapi import FastAPI
import time
from fastapi import Request
# 创建 FastAPI 应用实例,可配置标题、描述等文档信息
app = FastAPI(
title="FastAPI 入门示例",
description="第一个 FastAPI 程序,演示基础能力",
version="1.0.0"
)
@app.middleware("http")
async def calc_process_time(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
# 将耗时写入自定义响应头,单位秒
print(f"此次请求耗时{duration:.4f}秒")
response.headers["X-Process-Time"] = f"{duration:.4f}"
return response
# 追加到 main.py 中
@app.get("/items/{item_id}", summary="获取商品详情")
def get_item(item_id: int):
"""
- item_id: 路径参数,必须为整数,FastAPI 会自动做类型校验
"""
return {"item_id": item_id, "name": f"商品{item_id}"}
验证方式:调用任意接口后,查看响应头即可看到 X-Process-Time 字段。
curl -I http://127.0.0.1:8000/items/112
7.2.2 场景二:全局请求日志
统一记录所有接口的请求方法、路径、状态码、耗时,替代每个接口手写日志。
import time
import logging
from fastapi import Request
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.middleware("http")
async def access_log(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
# 记录日志:请求方法、路径、状态码、耗时
logger.info(
f"{request.method} {request.url.path} "
f"- Status: {response.status_code} "
f"- Cost: {duration:.4f}s"
)
return response
启动服务后访问任意接口,控制台会自动输出日志:
INFO:__main__:GET /health - Status: 200 - Cost: 0.0005s
7.2.3 场景三:简易全局鉴权
对需要登录的接口统一校验 Token,白名单路径(如文档、登录接口)直接放行。
from fastapi import Request
from fastapi.responses import JSONResponse
# 白名单:不需要鉴权的路径
WHITE_LIST = ["/docs", "/openapi.json", "/redoc", "/health", "/user/login"]
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
# 白名单路径直接放行
if request.url.path in WHITE_LIST:
return await call_next(request)
# 从请求头获取 Token
token = request.headers.get("Authorization")
if not token or not token.startswith("Bearer "):
return JSONResponse(
status_code=401,
content={"code": 401, "message": "缺少有效Token", "data": None}
)
# 简单校验(实际项目中替换为 JWT 解析逻辑)
real_token = token.split(" ")[1]
if real_token != "admin123":
return JSONResponse(
status_code=403,
content={"code": 403, "message": "Token无效或已过期", "data": None}
)
# 将用户信息存入 request.state,路由函数中可直接取用
request.state.user_id = 1
request.state.username = "admin"
return await call_next(request)
在路由函数中获取用户信息:
from fastapi import Request
@app.get("/user/info")
def get_current_user(request: Request):
# 直接从 request.state 读取中间件注入的用户信息
return {
"user_id": request.state.user_id,
"username": request.state.username
}
测试命令:
# 不带 Token,返回 401
curl http://127.0.0.1:8000/user/info
# 带正确 Token,正常返回
curl -H "Authorization: Bearer admin123" http://127.0.0.1:8000/user/info
注意:简单鉴权可以用中间件实现,复杂的权限控制更推荐使用 FastAPI 的依赖注入(Depends),粒度更细、可复用性更强。
7.3 内置核心中间件:CORS 跨域处理
前后端分离项目中跨域是必解问题,FastAPI 官方内置了 CORSMiddleware,无需额外安装依赖,几行配置即可解决跨域问题。
7.3.1 完整配置示例
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# 配置允许跨域的源(前端地址)
origins = [
"http://localhost:3000", # 前端开发地址
"http://localhost:8080",
"https://your-production.com", # 生产环境前端域名
]
# 注册 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=origins, # 允许的源列表
allow_credentials=True, # 允许携带 Cookie、Authorization 等凭证
allow_methods=["*"], # 允许所有 HTTP 方法(GET/POST/PUT/DELETE 等)
allow_headers=["*"], # 允许所有请求头
)
7.3.2 关键参数说明
| 参数 | 作用 | 注意事项 |
|---|---|---|
allow_origins |
允许跨域访问的前端域名列表 | 生产环境禁止使用 ["*"],存在安全风险 |
allow_credentials |
是否允许携带凭证(Cookie、Token) | 设为 True 时,allow_origins 不能为 * |
allow_methods |
允许的 HTTP 请求方法 | 常用 ["*"] 或 ["GET", "POST", "PUT", "DELETE"] |
allow_headers |
允许的请求头字段 | 常用 ["*"] 放行所有请求头 |
开发环境可以临时使用
allow_origins=["*"]快速调试,上线前必须替换为明确的域名列表。
7.4 多个中间件的执行顺序(高频易错点)
当注册多个中间件时,执行顺序遵循洋葱模型:
- 请求阶段:按注册的逆序执行(最后注册的最先执行)
- 响应阶段:按注册的正序执行(最先注册的最先执行)
示例验证
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def middleware_a(request: Request, call_next):
print("A 请求阶段")
response = await call_next(request)
print("A 响应阶段")
return response
@app.middleware("http")
async def middleware_b(request: Request, call_next):
print("B 请求阶段")
response = await call_next(request)
print("B 响应阶段")
return response
@app.get("/")
def root():
print("路由函数执行")
return {"message": "ok"}
控制台输出顺序:
B 请求阶段
A 请求阶段
路由函数执行
A 响应阶段
B 响应阶段
可视化执行流程:
请求 → B → A → 路由函数 → A → B → 响应
工程化最佳实践:注册顺序建议从外到内:CORS中间件 → 日志中间件 → 鉴权中间件 → 业务中间件。
7.5 集成到实战项目
将中间件能力整合到前文的实战项目中,修改 app/main.py 如下:
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
import time
import logging
from app.routers.user import user_router
from app.routers.upload import upload_router
from app.common.exceptions import BizException
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(
title="FastAPI 实战项目",
description="整合路由、校验、异常、中间件、统一响应的完整示例",
version="1.0.0"
)
# ========== 1. CORS 跨域中间件(最外层,最先注册) ==========
origins = [
"http://localhost:3000",
"http://localhost:8080",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ========== 2. 全局日志 + 耗时统计中间件 ==========
@app.middleware("http")
async def access_log_middleware(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Process-Time"] = f"{duration:.4f}"
logger.info(
f"{request.method} {request.url.path} "
f"- Status: {response.status_code} "
f"- Cost: {duration:.4f}s"
)
return response
# ========== 挂载路由 ==========
app.include_router(user_router)
app.include_router(upload_router)
# ========== 全局异常处理(同前文,此处省略重复代码) ==========
# ... 保留原有的异常处理器代码 ...
补充说明
中间件是全局生效的,适合处理全接口通用逻辑;如果只需要部分接口复用逻辑,更推荐使用 FastAPI 的依赖注入(Depends),粒度更灵活、测试性更好。
需要我继续补充依赖注入(Depends) 章节,或者讲解 JWT 认证、数据库集成等进阶内容吗?
八、完整实战项目:整合所有能力
本章节将路由管理、参数校验、全局异常、统一响应、中间件五大核心能力整合为一个可直接运行的工程化项目,采用模块化分层结构,符合企业级开发规范。
8.1 项目目录结构
fastapi_demo/
├── app/
│ ├── __init__.py
│ ├── main.py # 项目入口:注册中间件、异常处理器、挂载路由
│ ├── common/ # 公共组件层
│ │ ├── __init__.py
│ │ ├── response.py # 统一响应结构封装
│ │ └── exceptions.py # 自定义业务异常类
│ ├── middleware/ # 中间件层(新增)
│ │ ├── __init__.py
│ │ └── custom_middleware.py # 自定义业务中间件
│ ├── models/ # 数据模型层(Pydantic)
│ │ ├── __init__.py
│ │ └── user.py # 用户相关请求/响应模型
│ └── routers/ # 路由层
│ ├── __init__.py
│ ├── user.py # 用户模块接口
│ └── upload.py # 文件上传模块接口
└── requirements.txt # 项目依赖清单
8.2 各模块完整代码
8.2.1 依赖清单 requirements.txt
中间件为框架内置能力,无新增依赖:
fastapi>=0.110.0
uvicorn>=0.27.0
python-multipart>=0.0.9
pydantic>=2.6.0
8.2.2 公共组件层
app/common/response.py
from pydantic import BaseModel
from typing import Generic, TypeVar, Optional
T = TypeVar("T")
class ResponseResult(BaseModel, Generic[T]):
code: int = 200
message: str = "success"
data: Optional[T] = None
def success(data: T = None, message: str = "操作成功") -> ResponseResult[T]:
return ResponseResult(code=200, message=message, data=data)
def fail(code: int = 400, message: str = "操作失败") -> ResponseResult:
return ResponseResult(code=code, message=message, data=None)
app/common/exceptions.py
class BizException(Exception):
"""自定义业务异常类"""
def __init__(self, code: int, message: str):
self.code = code
self.message = message
super().__init__(message)
8.2.3 数据模型层
app/models/user.py
from pydantic import BaseModel, Field, field_validator
class UserCreate(BaseModel):
username: str = Field(min_length=3, max_length=20, description="用户名")
age: int = Field(ge=0, le=120, default=18, description="年龄")
phone: str = Field(pattern=r"^1[3-9]\d{9}$", description="手机号")
password: str = Field(min_length=6, max_length=32, description="密码")
@field_validator("password")
@classmethod
def check_password(cls, value: str) -> str:
if not any(c.isdigit() for c in value):
raise ValueError("密码必须包含至少一位数字")
return value
class UserResp(BaseModel):
user_id: int
username: str
age: int
phone: str
8.2.4 中间件层(新增)
app/middleware/custom_middleware.py
封装两个高频业务中间件,与入口文件解耦,符合工程化拆分规范:
import time
import logging
from fastapi import Request
from fastapi.responses import JSONResponse
logger = logging.getLogger(__name__)
# 鉴权白名单:无需登录即可访问的路径
AUTH_WHITE_LIST = [
"/health",
"/docs",
"/openapi.json",
"/redoc",
"/user/list",
"/user/create",
"/upload/avatar"
]
async def access_log_middleware(request: Request, call_next):
"""
全局访问日志中间件:统计接口耗时、记录请求全链路日志
"""
start_time = time.perf_counter()
# 将请求传递至下一层中间件/路由函数
response = await call_next(request)
# 计算接口执行耗时
process_time = time.perf_counter() - start_time
# 耗时写入响应头,前端可直接读取
response.headers["X-Process-Time"] = f"{process_time:.4f}"
# 控制台输出访问日志
logger.info(
f"{request.method} {request.url.path} "
f"| 状态码: {response.status_code} "
f"| 耗时: {process_time:.4f}s"
)
return response
async def auth_middleware(request: Request, call_next):
"""
全局鉴权中间件:白名单路径直接放行,其余路径校验身份凭证
"""
# 白名单路径跳过鉴权
if request.url.path in AUTH_WHITE_LIST:
return await call_next(request)
# 从请求头提取 Token
token = request.headers.get("Authorization")
# 格式校验
if not token or not token.startswith("Bearer "):
return JSONResponse(
status_code=401,
content={"code": 401, "message": "缺少有效身份凭证", "data": None}
)
# 提取 Token 主体(实际项目替换为 JWT 解析、Redis 校验等业务逻辑)
access_token = token.split(" ")[1]
if access_token != "fastapi_demo_token":
return JSONResponse(
status_code=403,
content={"code": 403, "message": "Token无效或已过期", "data": None}
)
# 鉴权通过,将用户信息注入 request.state,下游路由可直接读取
request.state.user_id = 1001
request.state.username = "demo_user"
return await call_next(request)
8.2.5 路由层
app/routers/user.py(原有基础上新增鉴权测试接口)
from fastapi import APIRouter, Path, Query, Request
from app.models.user import UserCreate, UserResp
from app.common.response import ResponseResult, success
from app.common.exceptions import BizException
user_router = APIRouter(prefix="/user", tags=["用户管理"])
@user_router.get("/list", summary="获取用户列表", response_model=ResponseResult)
def get_user_list(
page: int = Query(default=1, ge=1, description="页码"),
page_size: int = Query(default=10, ge=1, le=100, description="每页数量")
):
data = {
"page": page,
"page_size": page_size,
"total": 100,
"list": [{"user_id": i, "username": f"用户{i}"} for i in range(page_size)]
}
return success(data=data)
@user_router.get("/{user_id}", summary="获取用户信息", response_model=ResponseResult[UserResp])
def get_user(user_id: int = Path(..., gt=0, description="用户ID")):
if user_id > 1000:
raise BizException(code=1001, message="用户不存在")
user_data = {
"user_id": user_id,
"username": f"用户{user_id}",
"age": 20,
"phone": "13800138000",
"password": "123456" # 响应模型自动过滤敏感字段
}
return success(data=user_data)
@user_router.post("/create", summary="创建用户", response_model=ResponseResult)
def create_user(user: UserCreate):
# 模拟创建用户,返回自增ID
new_user_id = 1001
return success(data={"user_id": new_user_id}, message="用户创建成功")
@user_router.get("/me/info", summary="获取当前登录用户信息", response_model=ResponseResult)
def get_current_user(request: Request):
"""
需鉴权接口:从 request.state 读取中间件注入的用户信息
"""
user_info = {
"user_id": request.state.user_id,
"username": request.state.username
}
return success(data=user_info)
app/routers/upload.py
from fastapi import APIRouter, File, UploadFile
from app.common.response import ResponseResult, success
upload_router = APIRouter(prefix="/upload", tags=["文件上传"])
@upload_router.post("/avatar", summary="上传头像", response_model=ResponseResult)
async def upload_avatar(file: UploadFile = File(..., description="头像图片")):
content = await file.read()
data = {
"filename": file.filename,
"size": len(content),
"content_type": file.content_type
}
return success(data=data, message="上传成功")
8.2.6 项目入口 main.py(整合中间件、异常、路由)
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from app.routers.user import user_router
from app.routers.upload import upload_router
from app.common.exceptions import BizException
from app.middleware.custom_middleware import access_log_middleware, auth_middleware
# 创建应用实例
app = FastAPI(
title="FastAPI 全栈实战项目",
description="整合路由、参数校验、异常处理、中间件、统一响应的工程化示例",
version="1.0.0"
)
# ========== 1. 注册中间件(洋葱模型:先注册的在内层,后注册的在外层) ==========
# 1.1 CORS 跨域中间件(最外层,优先注册)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://localhost:8080"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 1.2 全局日志 + 耗时统计中间件
app.middleware("http")(access_log_middleware)
# 1.3 全局鉴权中间件(最内层,最后注册)
app.middleware("http")(auth_middleware)
# ========== 2. 挂载业务路由 ==========
app.include_router(user_router)
app.include_router(upload_router)
# ========== 3. 全局异常处理器 ==========
@app.exception_handler(BizException)
async def biz_exception_handler(request: Request, exc: BizException):
return JSONResponse(
status_code=200,
content={"code": exc.code, "message": exc.message, "data": None}
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
first_error = exc.errors()[0]
error_msg = f"参数[{'.'.join(map(str, first_error['loc']))}]:{first_error['msg']}"
return JSONResponse(
status_code=400,
content={"code": 400, "message": error_msg, "data": None}
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={"code": exc.status_code, "message": exc.detail, "data": None}
)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=500,
content={"code": 500, "message": "服务器内部错误", "data": None}
)
# ========== 4. 健康检查接口 ==========
@app.get("/health", summary="服务健康检查", tags=["系统接口"])
def health_check():
return {"status": "ok", "service": "fastapi-demo"}
8.3 项目启动与测试
在项目根目录执行启动命令:
uvicorn app.main:app --reload
8.3.1 基础功能测试
# 1. 健康检查
curl http://127.0.0.1:8000/health
# 2. 获取用户列表(白名单接口,无需鉴权)
curl "http://127.0.0.1:8000/user/list?page=2&page_size=5"
# 3. 创建用户(白名单接口,无需鉴权)
curl -X POST http://127.0.0.1:8000/user/create \
-H "Content-Type: application/json" \
-d '{"username":"lisi","age":25,"phone":"13800138000","password":"abc123"}'
# 4. 文件上传
curl -X POST http://127.0.0.1:8000/upload/avatar \
-F "file=@test.txt"
8.3.2 中间件能力测试
(1)验证耗时统计与日志
执行任意接口请求,查看响应头与控制台输出:
# 查看响应头中的耗时字段
curl -I http://127.0.0.1:8000/health
返回结果中会包含 X-Process-Time: 0.0005 类似字段,同时控制台自动输出访问日志。
(2)验证全局鉴权
# 不带Token访问需鉴权接口,返回401
curl http://127.0.0.1:8000/user/me/info
# 带错误Token访问,返回403
curl -H "Authorization: Bearer wrong_token" http://127.0.0.1:8000/user/me/info
# 带正确Token访问,正常返回用户信息
curl -H "Authorization: Bearer fastapi_demo_token" http://127.0.0.1:8000/user/me/info
(3)验证跨域配置
通过模拟预检请求验证跨域是否生效:
curl -X OPTIONS http://127.0.0.1:8000/health \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: GET"
返回头中包含 Access-Control-Allow-Origin 字段即代表配置生效。
还没结束,前面我们覆盖了路由、参数解析、校验、异常、统一响应、中间件这些工程化基础能力,接下来补充 FastAPI 最核心的进阶特性——依赖注入系统(Depends)。这是 FastAPI 区别于 Flask、Django 的核心优势之一,专门用来解决「接口间逻辑复用、业务解耦、权限粒度控制」的问题,也是企业级项目的标配写法。
九、依赖注入(Depends):业务逻辑复用与解耦
9.1 核心概念
依赖注入(Dependency Injection)本质是「把接口需要的公共逻辑抽离出来,由框架自动注入到路由函数中」,不需要我们手动实例化、传参。
它解决的核心痛点:
- 避免多个接口重复编写相同的逻辑(比如分页参数解析、Token 鉴权、数据库连接)
- 实现接口粒度的权限控制,比全局中间件更灵活
- 业务逻辑分层清晰,便于测试和维护
FastAPI 的依赖注入通过 Depends() 实现,支持函数依赖、类依赖、嵌套依赖等多种形式。
9.2 基础语法:最简单的函数依赖
依赖本身就是一个普通的 Python 函数,接收的参数规则和路由函数完全一致(支持路径参数、查询参数、请求头等),路由函数通过 Depends(依赖函数) 声明即可自动注入结果。
from fastapi import FastAPI, Depends
app = FastAPI()
# 定义依赖函数:解析通用分页参数
def get_pagination(page: int = 1, page_size: int = 10):
return {"page": page, "page_size": page_size}
# 在路由中声明依赖,框架自动执行依赖函数并把结果注入进来
@app.get("/items")
def get_items(pagination: dict = Depends(get_pagination)):
return {
"page": pagination["page"],
"page_size": pagination["page_size"],
"data": []
}
效果等价于:你在路由函数里手动写了 page 和 page_size 两个查询参数,但通过依赖把这段逻辑抽离了出来,所有需要分页的列表接口都可以一键复用。
9.3 高频实战场景
9.3.1 场景一:公共参数复用(分页、筛选)
这是最基础也最常用的场景,把分页、排序、关键词筛选这类通用查询参数抽成依赖,所有列表类接口直接复用,消除重复代码。
from typing import Optional
from fastapi import Query, Depends
# 通用分页筛选依赖
def common_query_params(
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(10, ge=1, le=100, description="每页条数"),
keyword: Optional[str] = Query(None, description="搜索关键词"),
order_by: str = Query("id", description="排序字段")
):
return {
"page": page,
"page_size": page_size,
"keyword": keyword,
"order_by": order_by
}
# 用户列表接口直接复用
@app.get("/user/list")
def get_user_list(params: dict = Depends(common_query_params)):
return {
"code": 200,
"message": "success",
"data": params
}
# 商品列表接口直接复用
@app.get("/goods/list")
def get_goods_list(params: dict = Depends(common_query_params)):
return {
"code": 200,
"message": "success",
"data": params
}
测试命令:
curl "http://127.0.0.1:8000/user/list?page=2&page_size=20&keyword=test"
9.3.2 场景二:接口级鉴权依赖(官方推荐方案)
全局中间件会拦截所有请求,但实际业务中往往是「部分接口需要登录,部分不需要」。用依赖注入可以精准控制到单个接口,粒度更细,也是 FastAPI 官方推荐的鉴权实现方式。
from fastapi import Header, HTTPException, Depends
# 鉴权依赖:校验请求头中的 Token
def verify_token(authorization: str = Header(..., description="身份凭证")):
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Token格式错误")
token = authorization.split(" ")[1]
# 实际项目替换为 JWT 解析、Redis 校验等业务逻辑
if token != "fastapi_demo_token":
raise HTTPException(status_code=403, detail="Token无效或已过期")
# 校验通过,返回用户信息,路由函数可直接使用
return {
"user_id": 1001,
"username": "demo_user"
}
# 需要登录的接口:声明依赖即可自动完成鉴权
@app.get("/user/me", summary="获取当前用户信息(需登录)")
def get_my_info(current_user: dict = Depends(verify_token)):
return {
"code": 200,
"message": "success",
"data": current_user
}
# 不需要登录的接口:不声明依赖,完全不受影响
@app.get("/public/list", summary="公开接口(无需登录)")
def get_public_list():
return {"code": 200, "message": "success", "data": []}
测试命令:
# 不带 Token,直接返回 422/401 错误
curl http://127.0.0.1:8000/user/me
# 带正确 Token,正常返回用户信息
curl -H "Authorization: Bearer fastapi_demo_token" http://127.0.0.1:8000/user/me
和全局中间件的核心区别:
- 中间件:全局生效,适合日志、跨域这类全接口通用的基础逻辑
- 依赖注入:接口粒度生效,适合鉴权、权限校验这类差异化的业务逻辑
实际项目中通常两者结合使用:中间件做全链路通用处理,依赖注入做业务级权限控制。
9.3.3 场景三:依赖嵌套
依赖之间可以互相嵌套,FastAPI 会自动解析完整的依赖链,非常适合分层的业务逻辑。
# 第一层依赖:从请求头提取原始 Token
def get_token(authorization: str = Header(...)):
return authorization.split(" ")[1]
# 第二层依赖:依赖第一层的 Token,解析并返回用户信息
def get_current_user(token: str = Depends(get_token)):
if token != "fastapi_demo_token":
raise HTTPException(status_code=403, detail="Token无效")
return {"user_id": 1001, "username": "demo"}
# 第三层:路由函数只依赖最终的用户信息
@app.get("/user/info")
def get_user_info(user: dict = Depends(get_current_user)):
return {"code": 200, "data": user}
9.4 进阶:类作为依赖
除了函数,类也可以作为依赖,适合状态更复杂、需要封装方法的场景,比如数据库会话、配置读取、业务服务类等。
class UserService:
"""用户服务类:封装用户相关的业务逻辑"""
def __init__(self, db_type: str = "mysql"):
self.db_type = db_type
def get_user_by_id(self, user_id: int):
# 模拟数据库查询
return {"user_id": user_id, "username": f"用户{user_id}", "db": self.db_type}
# 路由中注入类依赖,框架自动实例化
@app.get("/user/{user_id}")
def get_user(
user_id: int,
user_service: UserService = Depends(UserService)
):
user = user_service.get_user_by_id(user_id)
return {"code": 200, "data": user}
9.5 整合进实战项目
我们把依赖注入能力整合到前文的实战项目中,做两处核心优化:
- 抽离通用分页依赖,替换用户列表接口的重复参数
- 新增鉴权依赖,替换全局中间件的粗粒度鉴权,实现接口级精准控制
第一步:新增依赖文件 app/dependencies/common.py
# app/dependencies/common.py
from typing import Optional
from fastapi import Query, Header, HTTPException, Depends
def pagination_params(
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(10, ge=1, le=100, description="每页条数"),
keyword: Optional[str] = Query(None, description="搜索关键词")
):
"""通用分页查询依赖"""
return {
"page": page,
"page_size": page_size,
"keyword": keyword
}
def get_current_user(authorization: str = Header(..., description="身份凭证")):
"""鉴权依赖:校验Token并返回当前用户信息"""
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Token格式错误")
token = authorization.split(" ")[1]
if token != "fastapi_demo_token":
raise HTTPException(status_code=403, detail="Token无效或已过期")
return {
"user_id": 1001,
"username": "demo_user"
}
第二步:修改用户路由 app/routers/user.py
from fastapi import APIRouter, Path, Depends
from app.models.user import UserCreate, UserResp
from app.common.response import ResponseResult, success
from app.common.exceptions import BizException
from app.dependencies.common import pagination_params, get_current_user
user_router = APIRouter(prefix="/user", tags=["用户管理"])
@user_router.get("/list", summary="获取用户列表", response_model=ResponseResult)
def get_user_list(params: dict = Depends(pagination_params)):
# 直接使用注入的分页参数
data = {
"page": params["page"],
"page_size": params["page_size"],
"keyword": params["keyword"],
"total": 100,
"list": [{"user_id": i, "username": f"用户{i}"} for i in range(params["page_size"])]
}
return success(data=data)
@user_router.get("/{user_id}", summary="获取用户信息", response_model=ResponseResult[UserResp])
def get_user(user_id: int = Path(..., gt=0, description="用户ID")):
if user_id > 1000:
raise BizException(code=1001, message="用户不存在")
user_data = {
"user_id": user_id,
"username": f"用户{user_id}",
"age": 20,
"phone": "13800138000",
"password": "123456"
}
return success(data=user_data)
@user_router.post("/create", summary="创建用户", response_model=ResponseResult)
def create_user(user: UserCreate):
new_user_id = 1001
return success(data={"user_id": new_user_id}, message="用户创建成功")
@user_router.get("/me/info", summary="获取当前登录用户信息", response_model=ResponseResult)
def get_current_user_info(current_user: dict = Depends(get_current_user)):
# 依赖自动完成鉴权,直接使用注入的用户信息
return success(data=current_user)
第三步:调整 main.py 中间件配置
移除全局鉴权中间件,保留日志和跨域中间件,鉴权交由依赖注入精准控制:
# 只保留跨域和日志中间件,删除全局鉴权中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://localhost:8080"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.middleware("http")(access_log_middleware)
# 删除 app.middleware("http")(auth_middleware) 这一行
改造后项目架构更合理:
- 跨域、访问日志:全局中间件统一处理
- 鉴权、分页参数:依赖注入按接口按需使用
- 业务逻辑分层清晰,后续扩展新模块时可直接复用公共依赖
更多推荐
所有评论(0)