FastAPI系列-02-项目结构

FastAPI系列-02-项目结构

_

02 项目结构 —— 告别单文件,拥抱工程化目录

前置阅读: FastAPI系列-01-FastAPI介绍
关键词: 分层, 路由, 数据访问, 配置
难度: ★★★☆☆

场景导入:当“单文件一时爽”碰上“接口堆成山”

上一章结尾的那个 main.py 干净利落,几个端点看着很舒服。但现实是,接口数量很快就会涨到几十个,还要跟数据库、缓存、第三方服务打交道。所有逻辑平铺在一个文件里,找段代码得像大海捞针,改一处生怕牵动全身。

这时候,一份划分清晰的目录结构就不是“八股文”,而是你团队的协作地图:想看接口契约?进 schemas/;要改业务规则?找 services/;查 SQL 逻辑?直奔 dao/。本期我们为图书管理 API 规划了 Book、Author、Borrow 三个实体,并给出了一套可落地的目录模板,覆盖从入口到工具的各个角落。

下面就以 backend/ 这个模板为蓝本,聊聊分层思路、每个目录该装什么,以及如何从单文件版最小成本地演化到多模块版。

原理解析:把“对外协议”和“对内实现”拆开

分层的核心目的只有一个:让依赖方向保持单向,让每一层都能独立替换、独立测试。典型的 Web 工程会把职责拆成四到五层,咱们 FastAPI 项目也一样:

目录

承担的职责

入口

main.py

创建 FastAPI 应用、注册路由、加载 lifespan

接口层

app/api/

定义路径、处理请求与响应、调用 service

数据契约

app/schemas/

Pydantic 模型,负责请求体与响应体的字段约束

业务层

app/services/

把多个 dao 调用编排成业务用例(如借书、超期检测)

数据访问

app/dao/

仅与 ORM 或 SQL 交互,不感知 HTTP

持久化模型

app/models/

继承 DeclarativeBase 的实体类,与 MySQL 表结构一一对应

对应的目录树长这样(后续章节的代码都会基于它来展开):

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 模型。任何一层都不能反向依赖上一层。

下面的图能帮你直观地记住它:

flowchart TD A[api/books.py<br/>接口层] --> B[services/borrow_service.py<br/>业务层] A --> C[schemas/book.py<br/>Pydantic] B --> D[dao/book_dao.py<br/>数据访问] B --> E[dao/borrow_dao.py<br/>数据访问] D --> F[models/book.py<br/>ORM] E --> G[models/borrow.py<br/>ORM]

(图注:接口层 → 业务层 → 数据访问层 → ORM 模型,依赖单向流动)

classDiagram class ApiLayer { +APIRouter +include_router +依赖注入入口 } class ServicesLayer { +业务用例编排 +事务边界 +跨资源组合 } class DaoLayer { +SQL 拼装 +ORM Session 持有 +不感知 HTTP } class ModelsLayer { +Book / Author / Borrow +表结构映射 +关系约束 } ApiLayer --> ServicesLayer : 调用 ServicesLayer --> DaoLayer : 调用 DaoLayer --> ModelsLayer : 操作

(图注:四层职责与依赖关系,上层依赖下层,下层绝不反向感知上层)

这种单向依赖带来了两个直接好处。可测性:接口层可以轻松 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.pyapp/ 平级,让 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_routerbooks_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 持有 APIRouterservices/borrow_service.py 负责组合多个 dao 完成借书操作,dao/book_dao.py 只写 SQL。这三个目录用物理隔离强制了依赖规则,FastAPI 框架本身并不强制你分层,但这些约定会让项目长大以后依然可控。比如以后要加个借书超期提醒,改动基本只集中在 services/borrow_service.py,新增的 dao 方法通过依赖注入供接口层调用,ORM 模型和路由表完全不用动。

Q2 [项目]: 如果我要在这个模板上扩展完整的图书管理 API,目录应该怎么演化?

扩展顺序记住“由下至上、由内至外”这八个字就行,先把下层骨架补好,再往上推接口和业务规则。

具体来说:

  1. 数据库接入models/dao/ 会最先充实起来,models/book.pymodels/author.py 里定义 ORM 实体,dao/book_dao.py 接收 Session 参数并封装 CRUD。

  2. 新业务落地:像超期提醒这种跨资源的用例,会推动 services/ 出现独立模块,比如 services/borrow_service.py,它负责编排借书事务,组合多个 dao 调用并管理事务边界。

  3. 接口拆分:当端点越来越多,api/ 可以按业务域进一步划分成 api/books.pyapi/authors.pyapi/borrow.py,各自承载相关端点,然后在 main.py 里用 app.include_router 分别挂载。

落到文件上,你至少会新增这些物理文件:backend/app/models/book.pybackend/app/models/author.pybackend/app/dao/book_dao.pybackend/app/dao/author_dao.pybackend/app/services/borrow_service.py,以及 backend/tests/ 下与 app/ 结构镜像的测试入口。它们不会一次性加完,而是在不同章节按需引入:讨论 ORM 模型时引入 models,讨论查询时引入 dao,讨论借书用例时引入 services,第一次写分层单元测试时再建 tests/

小结 & 下篇预告

今天我们走通了 FastAPI 项目从单文件向分层演化的最短路径,固定了接口、业务、数据访问、ORM 模型这四大核心目录的职责边界。下一章《FastAPI系列-03-同步与异步》会深入 ASGI 与 WSGI 的差别,帮你彻底搞清楚 async defdef 在 FastAPI 里到底该怎么选——这可是写出高性能服务的关键一步,记得来看。

FastAPI系列-03-同步与异步 2026-06-12
FastAPI系列-01-FastAPI介绍 2026-06-06

评论区