1.1 从能用的接口到可维护的项目
从能用的接口到可维护的项目
FastAPI 很容易让我们在几分钟内得到一个能返回 JSON 的接口。真正的维护困难通常会稍后出现:文章列表、文章详情、作者信息和权限判断都放进同一个文件;某次字段修改牵连了不相关的接口;新同事不知道应该从哪里启动项目。项目一开始不需要几十个文件,但应该先把可观察的接口边界说清楚。
这一章先做一个“能运行、能检查、但故意很小”的文章管理 API。它只提供健康检查、文章列表和文章详情三个只读接口。这样我们可以先确认请求如何进入应用、路由如何返回结果,再在下一节按业务领域拆分文件。拆分不是为了让目录看起来复杂,而是为了让一次修改有可预测的影响范围。
先写清楚本节的接口契约
案例中的文章暂时只放在 Python 内存里,数据用于教学,不代表持久化方案。服务进程重启后,内存内容会回到代码中的初始值;多个 worker 进程也不会自动共享这份列表。因此,这个实现适合学习路由边界和测试方法,不能直接当作生产数据库。
本节约定如下:
| 请求 | 成功结果 | 失败结果 |
|---|---|---|
GET /health |
200,返回 {"status":"ok"} |
本接口没有业务失败分支 |
GET /posts |
200,返回文章数组 |
本节不把空列表当异常 |
GET /posts/{post_id} |
200,返回一篇文章 |
不存在时返回 404 |
文章对象先固定为 id、title、content 和 author_id 四个字段。author_id 只是数据关联的线索,本节不查询用户,也不声称已经实现身份认证。把未实现的能力写成“已支持”,会让后面的代码和文档产生错误预期。
准备练习环境
教程路线支持 Python 3.11 及以上版本。本批验证环境为 Python 3.12.13、FastAPI 0.141.1、Starlette 0.52.1、Pydantic 2.13.5、Uvicorn 0.52.4、HTTPX 0.28.1、AnyIO 4.15.0;其他 Python 3.11+ 环境仍应单独核对依赖和运行结果。版本号写出来是为了让验证结果可复现;不要把“安装最新版”当作稳定的依赖声明。
在教程项目外新建练习目录,然后创建虚拟环境:
mkdir fastapi-best-practices-lab
cd fastapi-best-practices-lab
python3 -m venv .venv
source .venv/bin/activate
python -m pip install
"fastapi==0.141.1"
"starlette==0.52.1"
"pydantic==2.13.5"
"uvicorn==0.52.4"
"httpx==0.28.1"
"anyio==4.15.0"
创建 ch01_start 目录,把下面的完整文件保存为 ch01_start/main.py。示例使用 def 路由,是为了先保持代码和执行方式简单;同步与异步的选择会在第二章单独解释。这里没有先创建 routers、services 或 models 空目录,暂时没有职责的文件只会增加寻找成本。
from fastapi import FastAPI, HTTPException
app = FastAPI(title="文章管理 API(第一章基线)")
## 教学用内存数据:重启进程后恢复初始值,不提供持久化能力。
posts: list[dict[str, object]] = [
{
"id": 1,
"title": "FastAPI 项目从哪里开始",
"content": "先定义可检查的接口,再决定如何拆分目录。",
"author_id": 1,
},
{
"id": 2,
"title": "让错误结果也有约定",
"content": "找不到文章时返回明确的 404,而不是返回空对象。",
"author_id": 2,
},
]
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.get("/posts")
def list_posts() -> list[dict[str, object]]:
return posts
@app.get("/posts/{post_id}")
def get_post(post_id: int) -> dict[str, object]:
for post in posts:
if post["id"] == post_id:
return post
raise HTTPException(status_code=404, detail="文章不存在")
从练习项目根目录启动时,工作目录决定了 main:app 的含义:这里必须进入 ch01_start,因为 main.py 就在当前目录。--reload 只适合开发期间自动重载文件,不是生产部署方案。
cd ch01_start
python -m uvicorn main:app --reload
另开终端并激活同一个虚拟环境,先看健康检查,再看列表和不存在的详情:
curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/posts
curl -i http://127.0.0.1:8000/posts/999
预期分别能看到 {"status":"ok"}、包含两篇文章的数组,以及状态行 404 Not Found 和 {"detail":"文章不存在"}。如果访问 /posts/1 时得到 404,先检查 URL 中的整数路径参数;如果命令提示无法导入 main,通常是没有在 ch01_start 目录启动,或者文件名不是 main.py。
用 TestClient 留下一条最小验收线
手动 curl 能帮助理解请求,但每次改代码都重复操作很容易漏掉 404 分支。下面的检查不需要 pytest,直接用 FastAPI 提供的 TestClient 在进程内调用 ASGI 应用。它验证四件事:健康状态、列表非空、文章有标题,以及不存在的文章确实返回 404。
from fastapi.testclient import TestClient
from main import app
def main() -> None:
with TestClient(app) as client:
health = client.get("/health")
assert health.status_code == 200
assert health.json() == {"status": "ok"}
posts = client.get("/posts")
assert posts.status_code == 200
items = posts.json()
assert items
assert all("title" in item for item in items)
detail = client.get("/posts/1")
assert detail.status_code == 200
assert detail.json()["title"] == "FastAPI 项目从哪里开始"
missing = client.get("/posts/999")
assert missing.status_code == 404
print("第一章基线检查通过")
if __name__ == "__main__":
main()
停止正在运行的 Uvicorn 后,在 ch01_start 目录执行:
python check.py
检查脚本通过,只能说明这个内存基线的四个行为符合约定,不能说明数据库、并发、认证或部署已经验证。后续章节会逐步增加这些边界,并继续保留能快速发现回归的检查。
下一节将把同样的三个接口拆到 posts 业务领域,比较按技术类型放文件和按业务领域放文件的维护差异。
来源与相邻小节
- 参考资料:fastapi-best-practices 中文 README 的“项目结构”部分。本节的文章案例、接口契约和检查脚本为教程新增示例。
- 框架参考:FastAPI First Steps。
- 下一节:按业务领域组织代码
阅读相邻小节时,请在教程目录中选择对应标题。