跳到主要内容

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 中查询学生列表接口返回 code、message、data 统一结构

五、测试验证​

打开 Swagger 文档刷新页面,可以看到接口的示例响应已经变成统一结构。

查询列表:点击 Try it out → Execute,返回体中包含 code、message,原始数据被包在 data 里。

查询单个学生:填写存在的 id,请求成功后同样返回 code / message / data 结构。

查询单个学生成功时返回统一响应结构

新增学生:填写信息后执行,返回 code 为 201、message 为 created,data 为新增的原始数据。

新增学生接口返回 code 201、message created 的统一结构

修改学生:填写 id 并修改字段后执行,返回 code、message 与包装后的 data。

修改学生接口返回统一响应结构

六、小结与遗留问题​

本文通过泛型实现了统一 API 响应结构:

  • APIResponse[T] 中的 T 是类型占位符,适配不同接口的数据类型;
  • ok 与 created 两个类方法分别对应 200 和 201 返回码;若返回码相同,定义一个即可;
  • 各接口通过 response_model 声明统一结构,返回时用类方法包装原始数据。

注意:以上只覆盖了请求成功的情况。测试一个不存在的 id(如 100)时会发现,失败请求返回的仍是 detail 结构,因为它走的是 HTTPException 异常分支,不会经过 APIResponse。异常情况的统一处理将在下一节介绍。

规划建议:可录制一段 GIF,展示在 Swagger 中依次点击查询、新增、修改接口并观察返回结构变化的完整过程,便于读者直观感受改造前后的差异。

课程推荐

想系统学习这门课程?

你已阅读完本文。需要更完整的知识体系,可以继续了解「FastAPI 项目实战」。

查看课程详情