02 项目结构 —— 告别单文件,拥抱工程化目录
前置阅读: FastAPI系列-01-FastAPI介绍
关键词: 分层, 路由, 数据访问, 配置
难度: ★★★☆☆
场景导入:当“单文件一时爽”碰上“接口堆成山”
上一章结尾的那个 main.py 干净利落,几个端点看着很舒服。但现实是,接口数量很快就会涨到几十个,还要跟数据库、缓存、第三方服务打交道。所有逻辑平铺在一个文件里,找段代码得像大海捞针,改一处生怕牵动全身。
这时候,一份划分清晰的目录结构就不是“八股文”,而是你团队的协作地图:想看接口契约?进 schemas/;要改业务规则?找 services/;查 SQL 逻辑?直奔 dao/。本期我们为图书管理 API 规划了 Book、Author、Borrow 三个实体,并给出了一套可落地的目录模板,覆盖从入口到工具的各个角落。
下面就以 backend/ 这个模板为蓝本,聊聊分层思路、每个目录该装什么,以及如何从单文件版最小成本地演化到多模块版。
原理解析:把“对外协议”和“对内实现”拆开
分层的核心目的只有一个:让依赖方向保持单向,让每一层都能独立替换、独立测试。典型的 Web 工程会把职责拆成四到五层,咱们 FastAPI 项目也一样:
对应的目录树长这样(后续章节的代码都会基于它来展开):
backend/
├── main.py
└── app/
├── api/
│ └── books.py
├── dao/
│ └── book_dao.py
├── models/
│ └── book.py
├── schemas/
│ └── book.py
├── services/
│ └── borrow_service.py
└── utils/
app/config/ 负责读取环境变量并暴露配置对象,app/utils/ 放一些与具体业务无关的工具函数(时间处理、签名校验、加密)。整套设计的依赖方向非常清晰:接口层依赖业务层,业务层依赖数据访问层,数据访问层依赖 ORM 模型。任何一层都不能反向依赖上一层。
下面的图能帮你直观地记住它:
(图注:接口层 → 业务层 → 数据访问层 → ORM 模型,依赖单向流动)
(图注:四层职责与依赖关系,上层依赖下层,下层绝不反向感知上层)
这种单向依赖带来了两个直接好处。可测性:接口层可以轻松 Mock 掉 service,业务层也能换用内存版 dao 跑单元测试。可替换性:将来你想把数据访问层换成异步驱动或读写分离代理,接口层和业务层一行代码都不用动。
记住一个经验法则:api/ 不应知道 ORM 的存在,services/ 不应关心 Pydantic 的字段标签,dao/ 根本不该听到 HTTP 状态码。
目录里还有个隐藏角色 app/db.py,它负责搭建异步基础设施:从环境变量读配置,用 create_async_engine 创建引擎,用 async_sessionmaker 生成 AsyncSessionLocal,并导出 get_async_db 这个异步依赖以及启动建表协程 init_db。细节会在第 13 章展开,你只要知道它是一切数据库操作的起点就行。
其他设计细节也值得留意:main.py 和 app/ 平级,让 app 成为一个标准 Python 包,导入路径自然写成 from app.api.books import ...;.env.dev 与 .env.prod 用来区分环境,通过启动命令切换,绝不硬编码;test_main.http 是 JetBrains 系编辑器的 HTTP 文件,可以在本地轻松替代 Postman 做接口调试。像 .venv/、.idea/、__pycache__/ 这些本地产物,都已经安静地躺在了 .gitignore 里。
代码实现:从单文件到多模块的最小改动
下面演示如何把上一章 main.py 里的 list_books 拆出来,落到 app/api/books.py,并把数据读取下沉到 app/dao/book_dao.py。(schemas/ 的内容到第 06 章再详细展开,这里先用空列表占位。)
# app/dao/book_dao.py
from typing import Optional
class BookDao:
"""数据访问层占位,后续章节接入 SQLAlchemy"""
def list_books(self, author_id: Optional[int] = None) -> list:
return []
# app/api/books.py
from fastapi import APIRouter
from app.dao.book_dao import BookDao
router = APIRouter(prefix="/books", tags=["books"])
_dao = BookDao()
@router.get("")
async def list_books() -> list:
return _dao.list_books()
# main.py
from fastapi import FastAPI
from app.api.books import router as books_router
app = FastAPI(title="图书管理 API", version="0.1.0")
app.include_router(books_router)
代码虽然短,但处处是心思:
BookDao现在是数据访问的入口,后面章节会改造成接收Session参数的版本。接口层里用
_dao = BookDao()做了一个模块级单例,小型项目够用;一旦项目变大,第 10 章的依赖注入会用Depends来优雅替代。main.py只剩下核心骨架:include_router把books_router挂载到应用上,所有以/books开头的请求都会自动进入app/api/books.py。tags=["books"]可不是摆设,它会告诉 OpenAPI 生成文档时把所有相关端点归到同一组,接口文档一目了然。
这些坑我替你踩过了
循环导入是头号杀手:
api/和services/千万不要互相引用。接口层导入业务层天经地义,但业务层绝不能反向依赖接口层,否则一跑测试就可能撞上模块加载死锁。不要直接把 ORM 对象丢给接口层:
dao/返回的如果是 SQLAlchemy 实例,响应体里就会混入一大堆内部状态,以后想换个序列化策略都困难。统一通过schemas/定义的 Pydantic 模型来收口。启动操作别写在模块级别:连接池预热、健康检查这类操作,务必放在
lifespan上下文里。如果随手写在模块级别,多次导入就会触发多次副作用,线上排查起来特别痛苦。
面试 QA:分层架构,咱们掰开揉碎说
Q1 [原理]: 后端工程为什么一定要做接口、业务、数据访问三层分层?
这个问题其实在问“分层的价值”。答案很简单:让依赖方向保持单向,让每一层都能独立替换、独立测试。
接口层只管协议格式和状态码,业务层只关心用例规则,数据访问层只负责持久化操作。想把 MySQL 换成 PostgreSQL?改 dao/ 和 models/ 就行;想把 REST 接口换成 GraphQL?只需动 api/ 那一层;业务规则调整?只改 services/ 下的文件。其他层纹丝不动。
落到咱们的项目里,你可以在源码中看到清晰的边界:api/books.py 持有 APIRouter,services/borrow_service.py 负责组合多个 dao 完成借书操作,dao/book_dao.py 只写 SQL。这三个目录用物理隔离强制了依赖规则,FastAPI 框架本身并不强制你分层,但这些约定会让项目长大以后依然可控。比如以后要加个借书超期提醒,改动基本只集中在 services/borrow_service.py,新增的 dao 方法通过依赖注入供接口层调用,ORM 模型和路由表完全不用动。
Q2 [项目]: 如果我要在这个模板上扩展完整的图书管理 API,目录应该怎么演化?
扩展顺序记住“由下至上、由内至外”这八个字就行,先把下层骨架补好,再往上推接口和业务规则。
具体来说:
数据库接入:
models/和dao/会最先充实起来,models/book.py、models/author.py里定义 ORM 实体,dao/book_dao.py接收Session参数并封装 CRUD。新业务落地:像超期提醒这种跨资源的用例,会推动
services/出现独立模块,比如services/borrow_service.py,它负责编排借书事务,组合多个 dao 调用并管理事务边界。接口拆分:当端点越来越多,
api/可以按业务域进一步划分成api/books.py、api/authors.py、api/borrow.py,各自承载相关端点,然后在main.py里用app.include_router分别挂载。
落到文件上,你至少会新增这些物理文件:backend/app/models/book.py、backend/app/models/author.py、backend/app/dao/book_dao.py、backend/app/dao/author_dao.py、backend/app/services/borrow_service.py,以及 backend/tests/ 下与 app/ 结构镜像的测试入口。它们不会一次性加完,而是在不同章节按需引入:讨论 ORM 模型时引入 models,讨论查询时引入 dao,讨论借书用例时引入 services,第一次写分层单元测试时再建 tests/。
小结 & 下篇预告
今天我们走通了 FastAPI 项目从单文件向分层演化的最短路径,固定了接口、业务、数据访问、ORM 模型这四大核心目录的职责边界。下一章《FastAPI系列-03-同步与异步》会深入 ASGI 与 WSGI 的差别,帮你彻底搞清楚 async def 和 def 在 FastAPI 里到底该怎么选——这可是写出高性能服务的关键一步,记得来看。