前言

接触过 Web 后端开发的人,大概率都写过"学生管理系统"——它几乎是 CRUD(增删改查)练习的经典场景。但同样的需求,不同的技术选型会带来截然不同的开发体验。这次我选择用 FastAPI 从零搭建一个学生信息管理 API,不使用任何数据库,而是用内存列表完成数据存储,并让框架自动生成 Swagger 接口文档。整个项目从设计到测试跑通,只用了不到一百行核心代码,却完整覆盖了接口设计、数据校验、分页过滤、单元测试等工程化要点。

这篇文章会完整还原项目的设计思路与实现细节,希望能为正在学习 FastAPI 或准备做课程设计的同学提供一份可参考的样板。

为什么选择 FastAPI

先说说技术选型。Python 生态里能做 Web 框架的选择很多:Django 功能全家桶,重量级但学习曲线陡峭;Flask 轻量灵活,但数据校验和接口文档都需要自己动手;而 FastAPI 恰好站在两者之间——它基于 Python 类型注解自动完成参数解析和数据校验,声明式地描述接口,并且原生内置 OpenAPI 支持,访问 /docs 就能获得一份可在线调试的交互式 API 文档。

FastAPI 还有几个非常契合本项目的优势:

第一,类型驱动。你只需要用 Python 标准类型注解定义函数参数,FastAPI 就能自动完成请求体的解析、类型转换、参数校验,非法请求会自动返回 422 状态码和详细的错误信息,省去了大量手写校验代码的精力。

第二,效率极高。FastAPI 基于 Starlette 构建,底层走 ASGI 异步通道,性能在 Python Web 框架中名列前茅。同时它的代码可读性很强,一目了然,便于维护。

第三,文档自动生成。Swagger UI 和 ReDoc 开箱即用,接口测试无需 Postman 也能完成,这对接口的联调和演示都极其方便。

基于这些理由,FastAPI 理所当然地成为了本项目的最佳选择。

项目整体设计

无论后端逻辑多复杂,一个好的项目首先要分层清晰。我把项目划分为四层文件:

bigDate_demo_0919/
├── app/
│   ├── main.py             # 应用入口
│   ├── models.py           # Pydantic 数据模型
│   ├── database.py         # 内存存储层
│   └── routers/
│       └── students.py     # 学生 CRUD 路由
├── tests/
│   └── test_students.py    # 单元测试
├── requirements.txt
└── README.md
  • models.py 定义数据契约:请求模型、响应模型、枚举类型,这是接口的"法律条文";
  • database.py 实现存储层:用列表保存数据,屏蔽了存储细节;
  • routers/students.py 负责路由与 HTTP 交互,把请求参数交给存储层处理;
  • main.py 完成应用装配:注册路由、配置文档元信息。

这样的分层把「接口层」和「存储层」解耦,未来如果要把内存存储替换成 MySQL,只需要改 database.py,路由层几乎不用动。

数据模型设计

学生信息应该包含哪些字段?我定义了这样一组:

  • id:唯一标识,系统自增分配
  • name:姓名,必填,限制 1~50 字符
  • age:年龄,必填,范围 1~150
  • gender:性别,用枚举限定 male / female / other
  • email:邮箱,选填,自动校验格式
  • phone:手机号,选填
  • major:专业,选填
  • class_name:班级,选填
  • created_at:创建时间,系统自动生成

在 Pydantic v2 中,这些约束通过 Field 声明:

class StudentBase(BaseModel):
    name: str = Field(..., min_length=1, max_length=50, description="学生姓名")
    age: int = Field(..., ge=1, le=150, description="学生年龄")
    gender: Gender = Field(..., description="学生性别")

ge 是 “greater than or equal”(大于等于),le 是 “less than or equal”(小于等于),Pydantic 会在请求进入业务逻辑之前完成全部校验。

邮箱格式校验不能用简单的长度约束表达,我使用了 field_validator 自定义校验逻辑:

@field_validator("email")
@classmethod
def validate_email(cls, value):
    if value is None or value == "":
        return None
    if "@" not in value or "." not in value.split("@")[-1]:
        raise ValueError("邮箱格式不正确")
    return value

这体现了一个重要设计思想:错误越早暴露,排查成本越低。与其让脏数据进入业务层最后出错,不如在接口边界就把它拦下来。

内存存储层:用列表实现数据库

用户要求用内存列表完成数据存储,这其实是一个相当务实的选择——不需要安装任何数据库、不需要建表,启动即用,特别适合演示和学习。

我实现了一个 StudentDatabase 类:

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

    def create(self, payload: StudentCreate) -> Student:
        with self._lock:
            student = Student(id=self._next_id, created_at=datetime.now(),
                              **payload.model_dump())
            self._students.append(student)
            self._next_id += 1
            return student

这里有两个容易忽略但很重要的细节:

第一个细节是自增 id。 我维护了一个 _next_id 计数器,每次创建学生都会分配当前的 _next_id,然后自增。这样即使删除了学生,新创建的 id 也不会与历史 id 重复,符合用户对主键稳定性的直觉。

第二个细节是线程安全。 FastAPI 的接口可能是并发处理的,如果多个请求同时往列表里写入数据,就可能出现竞态条件。我用 threading.RLock(可重入锁)保护所有读写操作,with self._lock: 包住整个临界区,保证任何时刻只有一个线程在修改数据。

此外,删除操作的实现用了一个小技巧:通过 pop(index) 从列表中移除元素,同时利用返回的布尔值告诉上层删除是否成功,比返回被删对象更直观。清空操作则调用了 clear(),并返回被清空的记录数。

存储层还额外提供了一份统计能力:遍历所有学生,计算总数、平均年龄、最大/最小年龄,统计性别分布和专业分布。这个接口在班级管理、学院管理的真实场景中非常常用,也展示了内存数据结构在处理聚合计算时的便利——sum(ages) / len(students) 一行就完成了平均年龄的计算。

路由层:让增删改查清晰可见

路由层的代码遵循 RESTful 风格设计,路径规划如下:

方法路径语义
POST/api/v1/students创建学生
GET/api/v1/students查询学生列表
GET/api/v1/students/stats统计信息
GET/api/v1/students/{id}查询单个学生
PUT/api/v1/students/{id}更新学生
DELETE/api/v1/students/{id}删除学生
DELETE/api/v1/students清空所有学生

每个接口都通过装饰器声明了 response_model、summary 和 description,这些信息会直接展示在 Swagger 页面上。

创建接口返回 201 状态码和完整的新学生对象;查询列表接口支持分页与五种过滤条件(姓名模糊、专业、性别、年龄区间),并以带 total、page、page_size、items 的分页模型返回,方便前端渲染分页组件;更新接口只更新请求中出现字段,其余保持不变;删除和详情接口在找不到目标时统一返回 404 与明确的中文错误信息。

这里我特别注意了一个路由声明顺序的问题:/stats 这样的静态路径必须声明在 /{student_id} 动态路径之前,否则 /api/v1/students/stats 会被误解析为 student_id="stats",导致校验失败。FastAPI 本身按声明顺序匹配路由,排好顺序能避免这类隐性 Bug。

分页逻辑同样值得说明。列表查询接受的参数是页码 page 和每页数量 page_size,返回时通过切片 items[start:start+page_size] 截取当前页数据。这个实现简洁明了,数据量大时换成 SQL 的 LIMIT/OFFSET 也是同样的思路。

用类型注解"免费"获得接口文档

整个项目最有"性价比"的部分,是接口文档完全不需要手写。

FastAPI 会扫描所有路由函数,根据参数类型和 response_model 自动生成 OpenAPI 规范。每个接口的注释、校验规则、枚举取值、字段说明,都会变成 Swagger UI 上的交互式表单。在浏览器打开 /docs,就能直接点击 “Try it out” 按钮发送真实请求、查看响应,接口联调的门槛一下降到了零。

在 openapi.json 中,所有接口的定义都以标准格式记录,这意味着你可以直接把这份规范导出,接入第三方 API 管理平台、生成前端 TypeScript 类型、或者喂给 Mock 工具做联调,生态价值很大。

单元测试:给接口穿上"安全网"

没有测试的接口改起来心里没底。我用 pytest 给项目配了 13 个用例,覆盖了全部核心路径:

  • 正常创建、创建时参数非法返回 422
  • 查询不存在的学生返回 404
  • 列表查询的姓名/专业过滤与分页
  • 部分字段更新
  • 统计接口的正确性
  • 删除与清空
  • Swagger 页面与 OpenAPI 规范可访问

测试用 fastapi 的 TestClient 模拟请求,配合 pytest.fixture 在每个用例执行前后清理内存数据,保证用例之间互不污染:

@pytest.fixture(autouse=True)
def clean_db():
    db.clear()
    db._next_id = 1
    yield
    db.clear()
    db._next_id = 1

运行 pytest tests/ -v,13 个用例全部通过。这种"写完直接跑测试"的闭环给了我很大的信心——后面任何一次重构,只要测试全绿,就说明接口行为没有被破坏。

实际运行效果

启动服务后,我用 curl 做了一遍端到端验证:

# 创建学生
curl -X POST http://127.0.0.1:8000/api/v1/students \
  -H "Content-Type: application/json" \
  -d '{"name":"李雷","age":20,"gender":"male","major":"计算机科学"}'

# 查询统计信息
curl http://127.0.0.1:8000/api/v1/students/stats

响应完全符合预期,统计接口正确算出总数、平均年龄、性别与专业分布。Swagger UI 返回 200,OpenAPI 规范中列出了全部 7 个业务路径和 2 个基础路径。整个服务从启动到可用,耗时不到 3 秒。

遇到的坑与解决

开发过程中也踩了不少坑,最典型的是两个:

第一个坑发生在路由参数上。我最初把路径里的 student_id 用 Query 声明,结果运行时报错 Cannot use 'Query' for path param。原因是路径参数必须使用 Path 声明,两者在 FastAPI 中不能混淆。修改后问题立刻解决。

第二个坑是 Swagger 风格测试的脆弱性。为了验证文档页可访问,测试里我判断了响应文本包含 “swagger” 关键字,这个断言依赖前端资源成功加载,在实际环境离线时可能不稳定。最终我保留了它,同时对项目的关键断言都补充了状态码检查,保证测试结果有更强壮的参照系。

第三个值得记录的细节是路由声明顺序。/api/v1/students/stats 如果放在 /{student_id} 之后,FastAPI 会把 “stats” 当作一个学生 id 去匹配,导致统计接口永远走不到。这类隐蔽问题即使代码正确也会让人调试很久,好在提前规避了。

从内存存储迈向真正的数据库

内存列表是学习阶段的完美起点,但真实业务显然不能止步于此。目前 database.py 已经把存储接口收敛得非常干净,演进路径很清晰:

第一,把 StudentDatabase 替换为基于 SQLAlchemy 或数据库驱动的实现,模型映射到关系表,增删改查改写为 SQL 语句,这是一对一的机械替换。

第二,把全局单例 db 改为 FastAPI 依赖注入,借助 Depends 在请求上下文传递存储实例,以后切换实现时路由层完全不需要改动。

第三,引入 Alembic 做数据库迁移,让表结构变更可追踪、可回滚。

这套"先用内存跑通逻辑,再平滑迁到数据库"的路径,正是很多小型项目从原型走向生产的标准姿势。内存存储提供的快速反馈,让设计者能先把精力放在接口契约和业务规则上,等一切都确定下来,再关注持久化方案——这也是一种值得提倡的开发节奏。

总结

这个项目虽然规模不大,却是一次完整的接口工程实践:从数据建模、分层设计、RESTful 路由、参数校验,到自动文档、单元测试和动手调试,涵盖了写一个真实 API 所需的全部关键动作。FastAPI 的类型注解哲学和自动文档能力,让原本繁琐的部分变得轻描淡写,也让我更深刻地理解了"约定优于配置"在接口开发中的价值。

对于想动手试一试的读者,我建议在自己的机器上运行这个项目,然后在 Swagger 页面里挨个 “Try it out”——当你能亲眼看到增删改查的响应变化时,对 REST 风格接口的理解会瞬间具象起来。项目代码已开源在仓库中,欢迎拆解、修改和扩展,比如为它加上数据库持久化、JWT 认证,或者对接前端页面。

愿你也能从这个小小项目中,收获属于自己的一套接口设计方法论。

Logo

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

更多推荐