FastAPI 统一 API 响应结构实战:用泛型封装成功与失败返回格式
在前后端分离的项目中,接口返回格式不统一是前端同学最头疼的问题之一。本文基于 FastAPI + Pydantic,带你用 Python 泛型实现一个统一的 API 响应结构,让所有接口(查询、新增、修改)都返回一致的 code / message / data 格式。
一、问题:返回格式五花八门
先看现状。以查询学生接口为例:
- 请求成功时,返回的是原始数据对象;
- 请求失败时,返回的又是另一种结构(如
detail字段)。
同样的问题也出现在新增、修改等接口上。前端调用时不得不针对每种格式分别处理,非常麻烦。
一个成熟的第三方天气 API 给了我们参考:无论成功还是失败,响应体都包含 message、status、code,真正的业务数据放在 data 字段里。这正是我们要对齐的目标。
目标:一次定义统一响应结构,所有接口统一调用,成功与失败都走同一套格式。
二、关键工具:Python 泛型
统一响应结构里的 data 字段类型是不固定的——有时是学生对象,有时是学生列表。写死类型显然不行,这时就要用泛型。
对比一下:
# 写死类型:只能处理字符串列表
def get_first(items: list[str]) -> str:
return items[0]
如果传入的是字典列表,编辑器提示的方法就全错了,必须手动改类型。
使用泛型(Python 3.12 之前需从 typing 导入 TypeVar,3.12 起可直接使用):
def get_first[T](items: list[T]) -> T:
return items[0]
T 是一个类型占位符:传入什么类型,返回值就是什么类型。编辑器能自动识别,传入字典列表时提示的就是字典方法,传入字符串列表时提示的就是字符串方法。
三、代码实现:定义 APIResponse 泛型类
在项目中新建统一响应类,继承 BaseModel,并使用泛型参数:
from pydantic import BaseModel
class ApiResponse[T](BaseModel):
code: int = 200
message: str = "success"
data: T | None = None
@classmethod
def ok(cls, data: T | None, message: str="success") -> "ApiResponse[T]":
return cls(code=200, message=message, data=data)
@classmethod
def created(cls, data: T) -> "ApiResponse[T]":
return cls(code=201, message="created", data=data)
要点说明:
code默认 200,message默认success,data默认为None;ok类方法用于请求成功场景,返回码 200;created类方法用于创建成功场景,返回码 201;- 返回类型标注加上引号(
"APIResponse[T]"),实现前向引用,避免类尚未定义完成时报错。
四、改造接口:response_model 与返回值
1. 查询学生列表
将 response_model 改为泛型响应,并把原始数据传给 ok:
@app.get("/api/v1/students", response_model=ApiResponse[list[Student]],tags=["学生管理"])
def get_students():
return ApiResponse.ok(data=fake_students_db)
2. 查询单个学生
@app.get("/api/v1/students/{student_id}", response_model=ApiResponse[Student],tags=["学生管理"])
def get_student(student_id: int):
for student in fake_students_db:
if student["id"] == student_id:
return ApiResponse.ok(student)
# 找不到抛出异常
raise HTTPException(status_code=404, detail=f"学生ID为{student_id} 不存在")
3. 新增学生
新增接口返回码为 201,因此调用 created:
@app.post("/api/v1/students", tags=["学生管理"],response_model=ApiResponse[Student],status_code=201)
def create_student(student: StudentCreate):
# 判断学号是否存在
for s in fake_students_db:
if s["student_no"] == student.student_no:
raise HTTPException(status_code=400, detail=f"学号{student.student_no}已经存在")
# 添加学生信息
new_id = max(s["id"] for s in fake_students_db) + 1
new_student = {"id": new_id, **student.model_dump()}
fake_students_db.append(new_student)
return ApiResponse.created(data=new_student)
4. 修改学生
@app.put("/api/v1/students/{student_id}", response_model=ApiResponse[Student], tags=["学生管理"])
def update_student(student_id: int, student: StudentUpdate):
for index, s in enumerate(fake_students_db):
if s["id"] == student_id:
# 获取更改后的学生信息
updated = {**s, **student.model_dump(exclude_unset=True)}
fake_students_db[index] = updated
return ApiResponse.ok(data=updated)
# 找不到抛出异常
raise HTTPException(status_code=404, detail=f"学生ID为{student_id} 不存在")
删除接口没有设置 response_model,也没有返回值,因此不需要额外处理。
五、测试验证
打开 Swagger 文档刷新页面,可以看到接口的示例响应已经变成统一结构。
查询列表:点击 Try it out → Execute,返回体中包含 code、message,原始数据被包在 data 里。
查询单个学生:填写存在的 id,请求成功后同样返回 code / message / data 结构。
新增学生:填写信息后执行,返回 code 为 201、message 为 created,data 为新增的原始数据。
修改学生:填写 id 并修改字段后执行,返回 code、message 与包装后的 data。
六、小结与遗留问题
本文通过泛型实现了统一 API 响应结构:
APIResponse[T]中的T是类型占位符,适配不同接口的数据类型;ok与created两个类方法分别对应 200 和 201 返回码;若返回码相同,定义一个即可;- 各接口通过
response_model声明统一结构,返回时用类方法包装原始数据。
注意:以上只覆盖了请求成功的情况。测试一个不存在的 id(如 100)时会发现,失败请求返回的仍是 detail 结构,因为它走的是 HTTPException 异常分支,不会经过 APIResponse。异常情况的统一处理将在下一节介绍。
规划建议:可录制一段 GIF,展示在 Swagger 中依次点击查询、新增、修改接口并观察返回结构变化的完整过程,便于读者直观感受改造前后的差异。
想系统学习这门课程?
你已阅读完本文。需要更完整的知识体系,可以继续了解「FastAPI 项目实战」。
查看课程详情