从零动手:用 FastAPI 三十行核心代码搞定学生信息增删改查接口

一、写在前面:为什么还要写一个"增删改查"?

在日常学习和求职准备中,很多人会问:现在框架这么多,脚手架工具一键就能生成 CRUD 接口,为什么还要自己动手写一遍?

我的答案是:增删改查是后端的"九九乘法表"。它虽然简单,却几乎囊括了后端开发的全部核心概念——数据模型的建模、存储层的设计、RESTful 接口的约定、参数的校验、错误处理、接口文档的产出,以及最基本的自动化测试。把这些基本功打扎实了,后面无论面对多复杂的业务系统,结构上都万变不离其宗。

这篇文章就带大家从零构建一个学生管理系统 API:基于 Python 的 FastAPI 框架,使用内存列表作为数据存储,用十几分钟的代码量实现对学生信息的完整增删改查,并白嫖 FastAPI 自带的 Swagger 交互式文档,最后再补上一套接口自动化测试。项目代码我已上传到 AtomGit,地址放在文末,欢迎对照阅读。

二、需求梳理与技术选型

2.1 需求分析

先把需求写清楚。我们要做一个面向学生信息管理的 RESTful 接口,围绕「学生」这个实体提供五类操作:

  1. 新增学生:提交姓名、年龄、性别、专业、邮箱等信息,系统分配唯一 ID;
  2. 查询列表:返回全部学生,最好支持关键字模糊搜索(按姓名或专业)和年龄区间过滤;
  3. 查询详情:根据 ID 查询单个学生;
  4. 更新信息:根据 ID 修改学生信息,且支持"只改部分字段";
  5. 删除学生:根据 ID 删除一条记录。

同时还有两个隐含需求:第一,需要给出 API 说明文档,方便前端同学或其他系统对接;第二,数据存储采用内存列表即可,不引入数据库——这让我们可以完全聚焦在接口开发本身,把复杂的存储问题留到以后扩展。

2.2 技术选型对比

Python 生态里做接口,绕不开三个选择:Flask、FastAPI、Django REST Framework。我最终选择了 FastAPI,理由如下:

  • 自动生成文档:FastAPI 基于类型注解自动生成 OpenAPI 规范,并内置 Swagger UI,访问 /docs 即可获得一个可点击调用的交互式文档,完美满足"给出 Swagger API 说明"的需求;
  • 类型校验开箱即用:配合 Pydantic,声明数据模型的同时就完成了参数校验,非法请求会自动返回 422 错误,不用手写一行校验代码;
  • 性能出色:基于 Starlette 和 ASGI,性能接近 NodeJS 和 Go;
  • 代码即文档:接口的路径、参数、返回结构全都在代码中声明,文档永远不会和代码脱节。

一句话总结选型思路:FastAPI 用最少的样板代码,把"定义接口"和"写文档"这两件事合二为一,非常适合中小型项目和教学演示。

三、项目结构与整体架构

动手编码前,先规划目录结构。虽然这是一个演示项目,我仍然采用了分层的思想,把不同职责的代码放在不同文件里,方便日后扩展:

bigDate_demo_0919/
├── app/
│   ├── __init__.py          # 应用包
│   ├── main.py              # 应用入口:创建 FastAPI 实例、注册路由
│   ├── models.py            # 数据模型层:Pydantic Schema 定义
│   ├── database.py          # 存储层:内存列表 + 线程锁
│   └── routers/
│       ├── __init__.py
│       └── students.py      # 路由层:学生增删改查接口
├── tests/
│   └── test_students.py     # 接口自动化测试
├── requirements.txt         # 依赖清单
├── run.py                   # 本地启动脚本
└── README.md                # 项目说明文档

分层设计带来的好处是:如果以后要把内存存储换成 MySQL,只需要修改 database.py 一个文件,路由层完全不用动;如果以后要新增"教师管理",在 routers/ 下加一个 teachers.py 再注册进 main.py 即可。这就是我们在真实项目里常说的"高内聚、低耦合"。

数据流向也很清晰:客户端请求 → 路由层(students.py)校验并分发 → 存储层(database.py)执行读写 → 返回结果。全程无数据库、无外部缓存,非常适合新手读懂每个环节。

四、编码实战:从模型到接口

4.1 第一步:定义数据模型

数据模型是整个系统的"地基"。我们用 Pydantic 定义三个模型:StudentCreate(创建时的请求体)、StudentUpdate(更新时的请求体)、Student(返回给客户端的完整学生信息)。

以 StudentCreate 为例,代码非常直观:

class StudentCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=50, description="学生姓名")
    age: int = Field(..., ge=1, le=150, description="学生年龄")
    gender: str = Field(..., pattern="^(男|女|其他)$", description="学生性别")
    major: str = Field(..., min_length=1, max_length=100, description="专业")
    email: str = Field(..., description="邮箱地址")

这里我只写了三样东西:字段类型、校验规则、字段说明。得益于 Pydantic,模型本身就是"活文档":

  • min_length / max_length 约束了字符串长度;
  • ge=1, le=150 约束了年龄必须在 1 到 150 之间;
  • pattern 用正则限制了性别只能取"男/女/其他"三个值。

当客户端传入 {"age": 0} 时,FastAPI 会自动返回带有详细错误信息的 422 响应,定位问题非常方便。

更新模型 StudentUpdate 和创建模型的区别在于——所有字段都设为可选(Optional[...]),这样前端只需要传需要修改的字段即可,配合后面的"部分更新"逻辑,体验很好。

4.2 第二步:实现内存存储层

存储层是数据读写的地方。虽然用的是最简单的内存列表,我依然加入了一个关键设计——线程锁。

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

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

为什么要加线程锁?因为 Uvicorn 默认支持并发处理请求,如果有多个请求同时对列表进行「读改写」操作,很可能出现数据竞争——比如两个请求同时读到 next_id = 1,各自创建出重复 ID 的学生。加锁之后,create、update、delete 这些写操作串行执行,数据一致性就有了保障。

ID 采用自增策略:每新增一条记录,_next_id 自增 1,保证 ID 唯一且连续。查询、更新、删除都通过遍历列表匹配 ID 来实现,逻辑简单直白,即使没接触过后端的人也能一眼看懂。

另外我在文件底部暴露了一个全局单例 store = MemoryStore(),路由层直接引用它,避免每个接口各搞一个存储实例——这也是内存存储一个重要的使用约束。

4.3 第三步:编写路由接口

这是全项目最核心、也最"无聊"(因为太简单)的部分。我们定义了一个 APIRouter,统一前缀为 /api/v1/students,然后按 RESTful 约定挂五个接口:

方法路径作用
POST/api/v1/students新增学生
GET/api/v1/students查询列表
GET/api/v1/students/{id}查询详情
PUT/api/v1/students/{id}更新学生
DELETE/api/v1/students/{id}删除学生

以更新接口为例看看 FastAPI 的表达力:

@router.put("/{student_id}", response_model=Student, summary="更新学生信息")
def update_student(student_id: int, data: StudentUpdate) -> Student:
    updated = store.update(student_id, data)
    if updated is None:
        raise HTTPException(status_code=404, detail=f"未找到 ID 为 {student_id} 的学生")
    return updated

几点值得注意的细节:

  1. 路径参数 student_id: int 声明类型后,FastAPI 会自动做类型转换和校验,传非数字直接返回 422;
  2. response_model=Student 声明返回结构,FastAPI 会自动过滤掉不在模型中的字段,避免敏感数据外泄;
  3. 404 处理:查询不到对应 ID 时抛出 HTTPException,返回规范的错误信息;
  4. 更新逻辑:在存储层用 data.model_dump(exclude_unset=True) 只提取请求中实际传入的字段,再合并到原对象上,实现"部分更新"。

查询列表接口我还加了点"私货",支持两个查询参数:

def list_students(keyword: str | None = None, min_age: int | None = None, max_age: int | None = None):

keyword 同时对姓名和专业做模糊匹配,min_age / max_age 做年龄区间过滤。虽然需求里只说"查询列表",但加这两个参数几乎不用成本,却能让接口的实用性和演示效果提升一个档次。

4.4 第四步:组装应用入口

最后在 app/main.py 中创建 FastAPI 实例、挂载路由,并补充一个健康检查接口:

app = FastAPI(
    title="学生管理系统 API",
    description="基于 FastAPI 的简单学生信息管理接口...",
    version="1.0.0",
)
app.include_router(students.router, prefix="/api/v1", tags=["students"])

到这里,整个后端就写完了。核心代码不过一百多行——这就是 FastAPI 的魅力。

五、Swagger 文档:零成本的白嫖福利

现在启动服务试试。先安装依赖,再执行:

pip install -r requirements.txt
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

然后浏览器访问 http://127.0.0.1:8000/docs,一个排版精美、可交互的 Swagger UI 页面就呈现在眼前。这个页面能做的事情远超想象:

  1. 查看全部接口:五个 CRUD 接口按 students 分组排列,每个接口都展示方法、路径、摘要和详细说明;
  2. 在线调试:点击「Try it out」按钮,填写参数、点击「Execute」,接口就会被真实调用,请求与响应结果一目了然,完全不需要 Postman;
  3. 查看数据结构:请求体和响应体的字段定义、类型、校验规则、是否必填,都以 Schema 的形式结构化展示;
  4. 查看状态码:每个接口可能返回的 200、201、204、404、422 等状态码及含义都有说明。

这就是 FastAPI “代码即文档"哲学的具象化:文档不是额外维护的,而是由代码自动生成、与代码永远保持一致的。对团队协作而言,前后端都不用再为"文档过期”"参数对不上"这类问题扯皮。

除了 Swagger UI,FastAPI 还免费赠送 ReDoc(/redoc,另一种风格的文档)和 openapi.json(机器可读的接口规范,可直接导入其他工具)。

六、用 pytest 给接口上保险

接口写完了,文档也有了,但如果每次改代码都要手动打开 Swagger 点一遍,人力和信心都撑不住。所以最后一步,我用 FastAPI 自带的 TestClient 写了一套自动化测试,覆盖全部接口的正常流程以及 404 异常场景。

测试的思路非常简单:测试创建成功返回 201 且 ID 自增正确;测试非法参数(空姓名、超范围年龄、非法性别)返回 422;测试列表与过滤条件;测试按 ID 查询;测试部分更新;测试删除后列表清空;测试不存在的 ID 全部返回 404;最后验证 Swagger 页面和 OpenAPI 文档可正常访问。

为了用例之间互不影响,我用一个 fixture 在每个用例前清空存储数据:

@pytest.fixture(autouse=True)
def clean_store():
    with store._lock:
        store._students.clear()
        store._next_id = 1

运行 pytest tests/ -v,结果一目了然:

=================== 12 passed in 0.56s ===================

12 个用例全部通过。这套测试的价值在于:以后无论谁改动了接口逻辑或数据模型,只要跑一遍测试就能立刻发现有没有破坏原有功能——这就是回归测试的意义。

七、总结与扩展思考

回顾一下,我们用一百多行核心代码,完成了一个具备完整 CRUD、参数校验、自动文档、并发安全、自动化测试的学生管理接口项目。整个过程覆盖了后端开发的绝大部分核心要素,非常适合作为入门练手项目。

当然,作为一篇负责任的博客,必须指出这个项目的"玩具属性":内存存储意味着服务重启后数据全部丢失,且无法支撑多进程部署。如果你希望在真实项目中使用,可以参考以下扩展路线:

  1. 数据持久化:把存储层替换为 SQLite/MySQL,或引入 SQLAlchemy ORM,路由层代码几乎零改动——这正是分层设计红利;
  2. 分页支持:学生多了以后,列表接口应该支持 page / page_size 参数;
  3. 鉴权认证:增加 JWT 或 OAuth2 登录,保护管理接口不被任意调用;
  4. 部署上线:打包成 Docker 镜像,配合 docker-compose 一键部署;
  5. 监控日志:接入结构化日志、健康检查探针、指标采集,构建可观测性。

从"能跑"到"能上线",中间还有很长的路,但这条路的起点,正是你亲手写下的那一个增删改查接口。

项目完整代码已上传至:https://atomgit.com/Yuuovo/bigDate_demo_0919,仓库内包含完整源码、README 和测试用例。如果你对代码有任何疑问,或觉得哪里可以做得更好,欢迎留言交流,也欢迎 fork 之后动手扩展自己的版本——把 CRUD 玩出花来,才算真正吃透了它。

Logo

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

更多推荐