用 FastAPI 半小时写一个带 Swagger 文档的学生管理接口:从零到能上线的完整实践

一、为什么选 FastAPI

做后端接口开发,Python 生态里可选择的方向其实不少:Django 全家桶功能齐全但略显笨重,Flask 灵活轻巧却需要自己拼装校验、文档等一堆零件。直到我体验过 FastAPI 之后,才真正觉得找到了"开箱即用"的平衡点。

FastAPI 自带三个让我爱不释手的能力:第一,基于 Python 类型注解自动生成 OpenAPI 规范,Swagger UI 文档零成本获得;第二,Pydantic 数据校验让请求参数的合法性检查变得极其优雅;第三,原生异步支持,在需要高并发时不会成为瓶颈。这次我准备写一个学生信息管理接口,用来演示增删改查的完整套路,正好把 FastAPI 的这些特性都用上。

二、需求梳理:我们要做什么

需求很朴素:对学生表进行增删改查,数据暂时用内存列表存储,不需要数据库。听起来简单,但"简单"恰恰是检验框架功底的试金石。真正动笔前,我先把需求拆成了几个要点:

一是数据模型。学生需要有哪些字段?在这个项目里我定义了姓名、年龄、性别、邮箱、班级、成绩六个业务字段,外加一个系统自动生成的 id。字段的约束也得想清楚:年龄必须是 1 到 120 之间的整数,成绩是 0 到 100 的浮点数,性别只能取"男"“女”“其他”,这些约束放在 Pydantic 模型里统一声明,写一遍就能在创建、更新所有入口复用。

二是接口设计。CRUD 对应五类操作:查询列表用 GET,新增用 POST,查询单个、更新、删除都走带路径参数的形式。列表查询还要考虑实际使用场景,加上姓名模糊搜索、班级过滤和分页三个能力。更新操作我特意拆成两种语义:PUT 表示全量替换,必须提交所有必填字段;PATCH 表示部分更新,传哪个字段就改哪个字段。这种设计在真实项目中非常常见,也是面试里经常被问到的问题。

三是文档要求。这是本次的重点需求之一,FastAPI 的天然优势就能体现出来。

三、项目结构:从小作坊到规范工程

很多初学者喜欢把所有代码塞进一个 main.py,几十行还好,规模一大就寸步难行。我这次按照分层思想把项目拆成了清晰的模块:

bigdata/
├── app/
│   ├── main.py               # 应用入口,创建 FastAPI 实例
│   ├── models.py             # Pydantic 数据模型
│   ├── storage.py            # 内存存储层
│   └── routers/
│       └── students.py       # 学生接口路由
├── tests/
│   └── test_students.py      # 单元测试
├── requirements.txt
└── README.md

路由经过 APIRouter 统一挂载到 /api/v1/students 前缀下,而存储逻辑被单独抽到 storage.py 里。这样设计的好处是:将来如果想从内存列表换成 MySQL,只需要重写 storage.py,路由层和模型层一行都不用动。这就是分层解耦带来的可维护性红利,代码写出来不是给自己看的,更是给半年后的自己看的。

四、核心代码解读

先看数据模型。Pydantic 的 Field 可以内联声明约束,非常直观:

class StudentBase(BaseModel):
    name: str = Field(..., min_length=1, max_length=50, description="学生姓名")
    age: int = Field(..., ge=1, le=120, description="年龄")
    gender: str = Field(..., pattern="^(男|女|其他)$", description="性别")
    score: float = Field(..., ge=0, le=100, description="综合成绩")

这里有几个细节:... 表示必填;ge 和 le 是 greater equal 和 less equal 的缩写,约束数值范围;pattern 直接写正则校验性别枚举。一旦某个请求的字段违反约束,FastAPI 会自动返回 422 状态码和结构化的错误信息,不需要我们手写一行校验代码。

StudentCreate、StudentUpdate、Student 三个模型分别对应新增请求、更新请求和响应结构。新增接口用 StudentCreate,更新接口里把每个字段都改成可选,天然就形成了"只改传入字段"的 PATCH 语义。

再看存储层。因为要用内存列表存数据,我封装了一个 StudentStore 类,把对列表的所有操作都收敛到一个地方:

class StudentStore:
    def __init__(self):
        self._students: list[Student] = []
        self._next_id = 1
        self._lock = threading.Lock()

    def create(self, payload: StudentCreate) -> Student:
        with self._lock:
            student = Student(id=self._alloc_id(), **payload.model_dump())
            self._students.append(student)
            return student

注意这里用了 threading.Lock。FastAPI 的生产模式(uvicorn 多 worker)下会有多个线程同时处理请求,如果直接对列表做增删操作,可能出现竞态条件,轻则数据不一致,重则抛异常。给关键操作加一把锁,成本极低,换来的是基本的线程安全,这个习惯值得每位后端开发者养成。

列表查询支持三个过滤维度:keyword 做姓名模糊匹配、class_name 做班级精确匹配,skip 和 limit 实现分页。实现上先过滤再切片,同时返回总数和当前页数据,完全满足前端表格组件的分页需求。

路由层是接口的门面:

@router.get("", response_model=StudentPage, summary="分页查询学生列表")
def list_students(keyword=None, class_name=None, skip=0, limit=20):
    ...

response_model 指定了响应的结构,FastAPI 会自动完成对象序列化,同时生成对应的 OpenAPI schema。每个接口的 summary 和 description 会被原样展示在 Swagger 文档里,这就是"接口文档零成本"的根基。

新增接口返回 201 状态码,删除成功返回 204 无响应体,资源不存在统一返回 404 并携带中文错误信息:{"detail": "学生 999 不存在"}。错误信息的措辞直接写给调用方看,友好且明确。

更新和删除的实现也值得展开讲讲。全量更新接口把 StudentCreate 直接转换成 StudentUpdate,整体替换存储里的记录;部分更新接口则利用 Pydantic 的 model_dump(exclude_unset=True) 特性,只提取客户端实际传入的字段做合并,其他字段原样保留。删除就比较干脆,定位到记录后从列表弹出,返回布尔值表示是否成功。

这里有个隐藏细节:StudentUpdate 的所有字段都声明为可选,但我们在 Pydantic 模型里保留了和新增时一致的约束规则。也就是说,客户端即使只传一个 score 字段,也必须满足 0 到 100 的区间限制,邮箱字段传了就必须校验长度。约束的一致性和可复用性,正是把校验规则收敛在模型层带来的最大收益。

细心的读者可能已经发现,整个路由层几乎没有出现"if 字段不合法就返回什么"的代码。这是因为 FastAPI 在进入函数体之前,就已经完成了参数解析和校验:查询参数不满足类型要求、请求体违反字段约束,统统在入口处拦截,返回结构化的 422 错误。我只需要在业务代码里处理"数据存在与否"这一种分支,职责边界非常清楚。

五、Swagger 文档:需求自动满足

启动服务后访问 http://127.0.0.1:8000/docs,映入眼帘的是完整的交互式 API 文档页面。所有接口按标签分组展示,每个接口都可点开查看请求参数、请求体示例和响应结构。最贴心的是 “Try it out” 按钮,可以不用写一行 curl 就在页面上直接发起真实请求调试接口,后端开发的日常幸福感直接拉满。

除了 Swagger UI,FastAPI 还提供了 ReDoc(/redoc)这种更适合阅读的文档风格,以及 openapi.json 这份标准的 OpenAPI 规范文件,可以导入 Postman、Apifox 等工具做进一步的协作调试。

这套文档不是我们手写的,而是 FastAPI 根据路由、模型、字段描述自动生成的。也就是说,文档永远和代码保持一致,再也不会出现"文档和代码对不上"的历史难题。这正是我反复强调"选型决定体验"的原因。

六、边界思考:这些坑你要知道

开发过程中有几个值得记录的边界点:

第一,内存存储的宿命。列表数据存在进程内存里,服务一重启一切归零。所以我在 README 里明确标注了"仅用于学习演示",并且在应用的文档描述里也做了同样的提示。如果业务真的需要持久化,下一步就是引入 SQLite 或 MySQL。

第二,中文 URL 编码。直接在浏览器地址栏或 curl 命令里拼中文查询参数,容易触发 “Invalid HTTP request received” 之类的报错,正确的做法是对中文做 URL 编码,例如"张三"编码后是 %E5%BC%A0%E4%B8%89。在测试脚本里,建议直接用 httpx 这类 HTTP 客户端库,由库自动完成编码,省心省力。

第三,PUT 和 PATCH 的取舍。PUT 是全量替换,语义上要求客户端提交完整对象,遗漏必填字段就会 422;PATCH 是部分更新,适合"只改成绩"这类高频场景。两者并用,接口的灵活性更好,也符合 RESTful 的惯例。

第四,状态码的严谨性。新增用 201 Created,删除用 204 No Content,资源不存在用 404,参数不合法用 422。细节处的状态码规范,恰恰能体现接口的工程化程度。

七、运行与实测演示

项目真正跑起来也毫不费劲。创建虚拟环境、pip install -r requirements.txt、然后一句 uvicorn app.main:app --reload,服务就在本地 8000 端口待命了。接下来我用 curl 做了一轮完完整整的实操验证:

先创建一个学生,请求体带上姓名、年龄、性别、邮箱、班级和成绩,接口立刻返回带自增 id 的完整记录;再创建一个学生,然后调用列表接口,能看到 total 字段统计出总条数,分页参数控制每页返回的规模;接着用 PATCH 只改成绩字段,响应里其余字段原样保留,只有 score 变化;最后 DELETE 删除记录,返回 204。整套流程一气呵成。

验证过程中我还专门试了约束校验的效果:把成绩传成 101 分,接口直接返回 422 和字段级错误提示;查询一个不存在的 id,返回 404 和"学生 999 不存在"的说明。用户体验和报错可读性,都在这些细节里体现出来了。

八、测试:从"能跑"到"靠谱"

写完代码不等于完事,我配套写了 18 个单元测试,覆盖创建、查询、列表过滤、分页、全量更新、部分更新、删除以及 404/422 等异常路径。核心做法是用 pytest fixture 在每个用例前清空存储,保证用例间互不干扰;借助 FastAPI 自带的 TestClient 发起真实 HTTP 请求,测试代码的风格和线上调用一致。

pytest tests/ -v 一键跑完,18 个用例全绿。测试的意义不只是现在证明代码没问题,更是为将来重构托底,任何时候改动了存储实现,跑一遍测试就能立刻知道有没有破坏对外行为。

九、写在最后

回顾这个小项目:需求明确、结构分层、模型约束自动校验、文档自动生成、关键路径有测试兜底,一个学生管理接口从零到可演示只花了很短的时间。FastAPI 让我深刻体会到,好的框架不是让简单的事变复杂,而是把复杂的事藏起来,把简单的事做得更优雅。

如果你也正在学习 Web 接口开发,不妨照着这个项目动手敲一遍:先跑通 CRUD,再动手加一个"按班级统计人数"的聚合接口,然后尝试把存储层替换成 SQLite。等这些都做完,你对接口开发的整体认知会上一个台阶。完整的源代码都托管在 atomgit.com/yjxyao/bigdata,欢迎克隆体验,也欢迎在评论区交流你的踩坑心得。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐