28. FastAPI 项目结构
项目结构要表达职责和依赖方向
第 27 篇按业务分组,本篇决定项目级文件放哪里。目录的目标是让新人能快速找到入口、配置、业务规则和测试,而不是追求层数。
推荐的中小型结构
fastapi-project/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── router.py
│ │ ├── deps.py
│ │ └── users.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py
│ │ └── security.py
│ ├── db/
│ │ ├── __init__.py
│ │ ├── session.py
│ │ └── models.py
│ ├── middleware/
│ │ ├── __init__.py
│ │ └── request_context.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── services/
│ │ ├── __init__.py
│ │ └── user_service.py
│ └── utils/
│ ├── __init__.py
│ └── text.py
├── tests/
├── migrations/
├── scripts/
├── .env.example
├── .gitignore
├── pyproject.toml
├── uv.lock
└── README.md这套结构的核心不是“文件夹越多越好”,而是把因不同原因变化的代码分开:URL 和 HTTP 参数变化时找 api,全局请求响应处理变化时找 middleware,路由拦截规则变化时找 api/deps.py,业务规则变化时找 services,数据库结构变化时找 db 和 migrations,接口输入输出变化时找 schemas,环境配置变化时找 core/config.py。utils 只收纳难以归入具体职责、却又确实通用的工具代码,不是默认落脚点。
它是一种适合中小型项目的参考结构,不是 FastAPI 强制规定的模板。下面逐项说明每个目录和文件的职责。
项目根目录与 app:划分项目边界
| 路径 | 干什么用 | 为什么这样设计 |
|---|---|---|
fastapi-project/ | 整个项目的根目录,包含应用代码、测试、迁移、依赖声明和项目说明。安装依赖、启动应用、执行测试通常都在这里进行。 | 固定工作目录后,.env、数据库相对路径和 app.main 等导入路径都有统一基准,减少“在某个目录能运行,换个目录就失败”的问题。 |
app/ | 存放应用运行时需要的 Python 代码,包括入口、路由、业务逻辑、数据库模型和配置。 | 把应用代码与测试、脚本、项目配置分开,代码边界更清楚,也便于使用 from app.xxx import ... 做明确的包导入。 |
app/__init__.py | 将 app 标记为 Python 包,使 app.main、app.api.users 等模块可以按包路径导入。学习阶段通常可以为空。 | 让包边界明确。不要在应用导入时就自动创建应用、连接数据库或注册大量模块,否则一个简单的 import app 也可能产生副作用和循环导入。 |
app/main.py | 应用组装入口:创建 FastAPI 实例或应用工厂,注册总路由、中间件、异常处理器和 lifespan,最终暴露 app 给 Uvicorn。 | 入口只负责“把各部件装起来”,不应堆满用户注册、订单计算和 SQL。这样启动方式统一,业务修改也不会不断改动入口文件。 |
app/main.py 中常见的内容可以概括为:
from fastapi import FastAPI
from app.api.router import router
app = FastAPI(title="学习项目")
app.include_router(router, prefix="/api/v1")这里只展示组装关系。随着项目增长,可以再加入中间件和 lifespan,但不需要把每个业务路由都直接写到 main.py。
app/api/:只处理 HTTP 接口层
| 路径 | 干什么用 | 为什么这样设计 |
|---|---|---|
app/api/ | 存放所有 HTTP 接口相关代码,包括路径、请求方法、参数声明、状态码、响应模型和路由依赖。 | 它是外部请求进入应用的边界。把 HTTP 细节集中在这里,业务层就不必依赖 Request、Response 等 Web 概念。 |
app/api/__init__.py | 将 api 标记为包,通常保持为空或只做很少的公开导出。 | 仅仅导入这个包不应该自动注册路由。路由是否生效应由 include_router 显式决定,避免隐藏副作用。 |
app/api/router.py | 汇总各业务路由,例如把 users.py、products.py 中的 router 注册到一个总 router 上。 | main.py 只需注册总路由,不必了解每个业务文件。新增业务时主要修改路由聚合处,入口保持稳定。 |
app/api/deps.py | 存放面向 HTTP 的共享依赖,例如获取当前用户、校验租户请求头或要求管理员身份。 | FastAPI 常用 Depends 承担路由“拦截器”的职责。依赖与路由分开后,可以按总路由、业务路由或单个接口复用,覆盖范围也更明确。 |
app/api/users.py | 定义用户相关接口,例如创建用户、查询用户、修改资料。它声明输入 Schema、输出 Schema 和所需依赖,然后调用用户 Service。 | 路由负责“接收 HTTP 请求并返回 HTTP 响应”,Service 负责“业务上应该怎样做”。这样 URL 调整不会带动业务规则重写,业务规则也能脱离 HTTP 单独测试。 |
例如,users.py 适合出现 APIRouter、@router.post()、Depends、response_model;不适合出现大段密码校验规则、复杂事务流程或散落的 SQL。业务继续增多时,可以新增 products.py、orders.py,而不是把所有接口塞回 users.py 或 main.py。
router.py 与 users.py 的职责也不同:前者是路由目录和装配点,后者是某个业务的具体接口实现。总前缀、业务前缀和具体操作路径会逐层拼接,不要重复书写。
app/core/:放应用级基础设施,不放业务杂物
| 路径 | 干什么用 | 为什么这样设计 |
|---|---|---|
app/core/ | 存放配置、安全算法等应用级、较低层的基础设施代码。这里的 core 不是“所有核心代码都往里放”的意思。 | 多个模块都可能使用配置和安全工具。让它们保持独立,可以避免基础设施反过来依赖路由或业务服务。 |
app/core/__init__.py | 将 core 标记为包,通常保持为空。 | 避免导入配置包时顺带初始化认证、数据库等无关功能。 |
app/core/config.py | 使用 pydantic-settings 定义和读取环境变量,例如应用名、调试开关、数据库地址,并提供统一的 get_settings()。 | 配置只有一个权威入口,便于类型检查、校验和区分开发、测试、生产环境;业务代码不再到处调用 os.getenv() 或硬编码连接信息。 |
app/core/security.py | 放通用的安全操作,例如密码哈希与校验、令牌签名与验证。 | 这些是与具体 HTTP 路由无关的通用能力,可以被登录、改密码等多个业务复用。 |
要注意,security.py 不等于“所有权限代码”。密码怎样哈希属于通用安全能力;“用户是否有权查看这笔订单”包含业务规则,更适合放在对应业务 Service 或明确的授权模块中。core 也不应逐渐变成塞满无关函数的 utils.py。
app/db/:管理数据库连接与持久化模型
| 路径 | 干什么用 | 为什么这样设计 |
|---|---|---|
app/db/ | 集中存放 SQLAlchemy Engine、Session 工厂、数据库依赖和 ORM 模型。 | 数据库资源有明确归属,路由和业务代码不必各自创建连接,也不会把连接参数散落到项目中。 |
app/db/__init__.py | 将 db 标记为包,通常保持为空或只暴露少量稳定接口。 | 不建议在这里导入所有模型并自动建表,否则导入顺序更难控制,也容易产生副作用。 |
app/db/session.py | 创建 Engine 和 SessionLocal,并提供 get_db() 之类的 FastAPI 依赖,在请求结束时关闭 Session。 | Engine 管理连接池,通常按进程复用;Session 表示一次工作单元,应按请求创建和关闭。两者生命周期不同,不能都做成随意共享的全局对象。 |
app/db/models.py | 定义 ORM 模型,例如 User 对应的表名、列、默认值、索引和关系。 | 模型面向数据库,而不是面向前端。表结构集中定义后,查询、迁移和关系维护都有统一依据。 |
session.py 中不要每次请求都重新创建 Engine,这会破坏连接池复用;也不要创建一个全局 Session 给所有请求共享,因为并发请求会相互污染事务状态。
models.py 里的字段变化不会自动修改已有数据库。模型说明“代码期望表是什么样”,migrations/ 中的迁移则描述“现有数据库怎样逐步变成那个样子”。模型较多后,再把单文件拆成 models/user.py、models/order.py 等文件,不必第一天就创建大量空文件。
app/middleware/:放中间件实现,在 main.py 注册
中间件适合处理围绕整个 HTTP 请求响应过程的公共逻辑,例如请求编号、访问日志、耗时统计和统一响应头。项目结构中要把实现位置与注册位置分开:自研中间件放在 app/middleware/,应用创建时在 app/main.py 显式注册。
| 路径 | 干什么用 | 为什么这样设计 |
|---|---|---|
app/middleware/ | 存放自研中间件,按职责拆成请求上下文、访问日志等模块。 | 中间件不再堆进入口文件,又能与业务路由和 Service 保持边界。 |
app/middleware/__init__.py | 将 middleware 标记为包,通常保持为空。 | 不要在导入包时自动注册中间件或导入 main.py,否则容易产生副作用和循环导入。 |
app/middleware/request_context.py | 创建请求编号、写入 request.state,并给响应补充对应响应头。 | 请求上下文是一个独立横切职责,有明确文件可供中间件注册和测试使用。 |
例如,app/middleware/request_context.py 可以只保存中间件函数:
from uuid import uuid4
from fastapi import Request
async def request_context(request: Request, call_next):
request_id = uuid4().hex
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response然后在 app/main.py 中注册:
from fastapi import FastAPI
from app.api.router import router
from app.middleware.request_context import request_context
app = FastAPI(title="学习项目")
app.middleware("http")(request_context)
app.include_router(router, prefix="/api/v1")app.middleware("http")(request_context) 是把已有函数交给装饰器注册,效果等同于在函数定义处使用 @app.middleware("http")。这样中间件模块不需要反向导入 main.py 中的应用对象;仅仅导入中间件文件也不会让它自动生效。
CORSMiddleware、GZipMiddleware 等现成中间件通常直接在 main.py 注册即可,不必为了“每种代码都有一个文件”再包装一层。自研中间件很少时,也可以先使用单个 app/middleware.py,增多后再拆成同名包。多层中间件的执行顺序、请求体处理等机制见第 25 篇,本篇只约束代码落点。
“拦截器”写在哪里:FastAPI 通常用依赖实现
FastAPI 没有名为 Interceptor 的统一内置组件。从其他框架迁移“拦截器”时,不要一律新建 interceptors/ 目录,而要先判断它拦截的范围和目的:
| 要解决的问题 | 推荐机制 | 代码位置 |
|---|---|---|
| 所有 HTTP 请求都要增加请求编号、日志或公共响应头 | 中间件 | app/middleware/,在 main.py 注册 |
| 一组路由或某个接口需要登录、租户检查、管理员身份 | Depends 依赖 | 共享依赖放 app/api/deps.py |
| 校验令牌、哈希密码等具体算法 | 普通函数 | app/core/security.py |
| 当前用户能否取消这笔订单、能否执行某个业务动作 | 业务规则 | 对应 Service 或明确的授权模块 |
例如,共享的路由依赖放在 app/api/deps.py:
from typing import Annotated
from fastapi import Header, HTTPException
def require_tenant(
x_tenant_id: Annotated[str | None, Header()] = None,
) -> str:
if not x_tenant_id:
raise HTTPException(status_code=400, detail="缺少 X-Tenant-ID")
return x_tenant_id如果整个业务总路由都需要这项检查,在 app/api/router.py 挂载:
from fastapi import APIRouter, Depends
from app.api.deps import require_tenant
from app.api.users import router as users_router
router = APIRouter(dependencies=[Depends(require_tenant)])
router.include_router(users_router, prefix="/users")只需要拦截一个接口时,把 Depends(require_tenant) 声明在该路径操作上即可,不要改成全局中间件再维护一串排除路径。路由需要依赖返回的当前用户或租户值时,应把它声明为参数依赖,而不是只放进 dependencies=[...]。只被单个业务使用的依赖也可以留在该业务路由附近,deps.py 主要承载跨路由共享的 HTTP 依赖;get_db() 仍可留在 db/session.py,不必为了集中而全部搬走。
app/schemas/:定义对外输入输出契约
| 路径 | 干什么用 | 为什么这样设计 |
|---|---|---|
app/schemas/ | 存放 Pydantic Schema,描述接口接收什么数据、返回什么数据,以及校验和序列化规则。 | API 契约与数据库表结构是两种不同职责。分开后,接口字段可以独立演进,也能避免把数据库内部字段直接暴露出去。 |
app/schemas/__init__.py | 将 schemas 标记为包,通常保持为空。 | 让导入路径明确,同时避免导入一个 Schema 时连带加载大量无关模块。 |
app/schemas/user.py | 定义用户相关 Schema,例如 UserCreate、UserRead、UserUpdate,分别服务于创建、响应和更新。 | 不同操作所需字段并不相同,拆分后能精确控制客户端可以提交什么、服务端允许返回什么。 |
不要把 ORM 的 User 模型直接当成所有接口的输入输出。例如:
- 数据库模型可能包含
password_hash,但UserRead不应返回它。 - 创建用户时需要客户端提交明文密码,但响应中不能原样返回密码。
id、created_at可以由服务端或数据库生成,不应因为模型里有这些列,就要求客户端创建时提交。- 更新资料时往往只允许修改部分字段,不能直接把客户端提交的所有键无筛选地写到模型上。
因此,Model 主要回答“数据怎样存储”,Schema 主要回答“外部怎样传入和看到数据”。FastAPI 的 response_model 还可以对响应字段做最后一道筛选。
app/services/:承载业务规则与用例流程
| 路径 | 干什么用 | 为什么这样设计 |
|---|---|---|
app/services/ | 存放注册、下单、取消订单等业务用例,以及重复数据检查、状态流转、事务编排等业务规则。 | 业务规则不绑定某个 URL 后,可以被 HTTP 接口、命令行脚本或测试复用,也更容易定位规则修改的位置。 |
app/services/__init__.py | 将 services 标记为包,通常保持为空。 | 避免导入 Service 包时自动执行其他业务逻辑。 |
app/services/user_service.py | 实现用户相关业务,例如检查邮箱是否已存在、调用密码哈希函数、创建用户对象,并在一个明确的业务边界内提交事务。 | 用户路由保持简洁,复杂规则有独立位置;测试可以直接构造 Session 调用 Service,不必每次模拟完整 HTTP 请求。 |
Service 通常接收已经构造好的参数和当前请求的 Session,而不是自己读取全局 Session,也不应直接返回 JSONResponse。它关注的是“业务是否允许、需要完成哪些写入”,HTTP 状态码和响应格式仍由 API 层决定。
不过,Service 也不是为了分层而做的机械转发。如果项目只有一个简单查询,额外包一层只执行同一行查询的函数未必有价值。出现重复规则、多个写入步骤、多个调用入口或独立测试需求时,Service 边界的收益会更明显。
app/utils/:只放真正通用的工具代码
Python 中所谓“工具类”通常不需要写成只有静态方法的 StringUtils、DateUtils 类。优先使用按职责命名的模块和普通函数,例如 app/utils/text.py;只有确实需要封装状态和统一接口时才定义类,而且要先判断它是否已有更明确的归属。
| 路径 | 干什么用 | 为什么这样设计 |
|---|---|---|
app/utils/ | 存放不依赖具体路由、数据库和业务规则的通用辅助代码。 | 给真正可复用且无明确业务归属的函数一个位置,同时避免它们散落复制。 |
app/utils/__init__.py | 将 utils 标记为包,通常保持为空。 | 不要在导入包时把所有工具互相导入,否则容易形成循环依赖和隐藏副作用。 |
app/utils/text.py | 放通用文本处理函数,例如折叠连续空白,而不是用户昵称的完整业务校验。 | 按能力命名文件比不断扩充一个 utils.py 更容易查找,也能减少杂物式堆积。 |
例如,app/utils/text.py 可以保持很简单:
def collapse_whitespace(value: str) -> str:
return " ".join(value.split())不要因为“多个地方都会调用”就把代码一律放进 utils。调用次数多不等于没有职责归属:
| 代码 | 更合适的位置 |
|---|---|
| 密码哈希、令牌签名与验证 | app/core/security.py |
| 获取当前用户、校验请求头等 HTTP 依赖 | app/api/deps.py |
| 请求编号、访问日志等请求响应包装 | app/middleware/ |
| 邮箱查重、注册流程、订单金额和状态规则 | 对应的 app/services/ 模块 |
| Engine、Session 和 ORM 查询相关辅助代码 | app/db/ 或明确的数据访问模块 |
只在用户模块中使用的小函数,可以先留在用户模块附近,出现真实复用需求后再提取。工具模块一旦需要依赖 Request、某个 ORM 模型或具体 Service,通常说明它已经不是通用工具,应按实际职责重新定位。也不要同时维护 utils/、helpers/、common/ 等多个含义相近的杂物目录。
tests/、migrations/、scripts/:三种非请求代码分开管理
| 路径 | 干什么用 | 为什么这样设计 |
|---|---|---|
tests/ | 存放接口测试、Service 测试以及 pytest 配置和夹具,例如 test_users.py、conftest.py。测试可以使用独立配置和独立数据库。 | 测试验证应用行为,但不属于线上请求处理代码。独立目录便于统一发现和执行,也能防止测试工具混入业务模块。 |
migrations/ | 存放 Alembic 等迁移工具生成的环境配置和版本脚本,记录建表、加列、改索引等数据库结构变化。 | 数据库结构需要按版本、可审查地应用到不同环境,不能只依赖启动时 create_all。迁移文件通常进入版本控制,并与对应模型变更一起审核。 |
scripts/ | 存放需要人工明确执行的一次性或运维脚本,例如历史数据导入、数据修复、初始化管理员。 | 这些任务与 Web 应用生命周期不同。独立存放可以避免它们在 import app、启动 worker 或处理普通请求时被意外执行。 |
三者不能混用:测试不应成为正式迁移手段,迁移不应夹带不可重复的大数据修复,临时脚本也不应通过复制一份业务代码来绕过 Service。脚本需要业务能力时,应尽量复用现有 Service 和数据库配置。
根目录文件:环境、依赖与协作约定
| 路径 | 干什么用 | 为什么这样设计 |
|---|---|---|
.env.example | 列出需要配置的环境变量名和无敏感示例值,例如数据库主机、端口、应用环境。该文件可以提交到版本控制。 | 新人能据此创建自己的 .env,又不会在示例中泄露真实密码、私钥或令牌。真正的 .env 保存本机或部署环境配置,不应提交。 |
.gitignore | 排除虚拟环境、Python 缓存、真实 .env、测试缓存、本地上传文件等不应共享的内容。 | 密钥不应进入仓库,自动生成或本机特有的文件也不应制造无意义变更。依赖锁文件不属于应忽略的临时产物。 |
pyproject.toml | 声明项目元数据、直接依赖、Python 版本要求,也可集中放置 pytest 等工具配置。 | 它表达“项目直接需要什么”,使用统一标准文件后,安装、构建和工具配置都有稳定入口。 |
uv.lock | 由 uv 生成,锁定直接依赖和间接依赖的解析结果及版本。通常不手工编辑,并随应用项目提交。 | 让开发、测试和部署能够复现同一套依赖解析结果,降低“我机器上可以运行”的差异。更新依赖时通过 uv 命令重新生成。 |
README.md | 说明项目用途、环境要求、安装步骤、变量配置、启动命令、测试和迁移方式。 | 它是协作者的项目入口。常用操作有统一说明,就不必靠口头传递,也不应把真实环境密钥写进去。 |
pyproject.toml 与 uv.lock 不是二选一:前者主要记录项目声明,后者记录本次完整解析结果。.env.example 与 .env 也不是重复文件:前者是可公开的模板,后者是具体环境的实际配置。
用一次“创建用户”请求串起所有层
以 POST /api/v1/users 为例,各文件大致这样协作:
- Uvicorn 从
app/main.py找到app,应用已经注册中间件和app/api/router.py中的总路由。请求先经过request_context,获得本次请求编号。 - 总路由找到匹配的路径操作,并按挂载范围执行
app/api/deps.py中的依赖,再把请求交给app/api/users.py。 - 路由使用
app/schemas/user.py的UserCreate校验请求体,并通过app/db/session.py的依赖获得本次请求的 Session。 - 路由调用
app/services/user_service.py。Service 检查用户名或邮箱是否重复,调用app/core/security.py对密码做哈希。 - Service 使用
app/db/models.py中的User模型组织要保存的数据,通过 Session 完成事务写入。 - 路由取得业务结果,使用
UserRead作为response_model,只返回允许公开的字段,不包含密码或密码哈希。响应返回阶段再经过中间件,补充X-Request-ID。 - 数据库依赖按自身生命周期关闭本次 Session,连接按连接池规则归还或管理。
这条链路可以概括为:
HTTP 请求
→ middleware/request_context.py 建立请求上下文
→ api/deps.py 按挂载范围执行路由依赖
→ api/users.py 解析 HTTP 输入、选择用例
→ services/user_service.py 执行业务规则和事务
→ db/models.py + Session 映射并保存数据
→ 数据库
数据库结果
→ Service 返回业务结果
→ schemas/user.py 筛选并序列化公开响应
→ middleware/request_context.py 处理返回阶段公共逻辑
→ HTTP 响应这样拆分后,修改通常有明确落点:改 URL 找 API,改全局请求上下文找 Middleware,改路由拦截规则找 Dependency,改重复邮箱规则找 Service,改响应字段找 Schema,改表字段找 Model 并新增迁移,改数据库连接参数找配置和 Session。不是每次变化都要修改所有层,例如只调整内部业务规则时,公开 Schema 可能完全不变;utils 也只有被某层实际调用时才进入链路,不是请求必须经过的一层。
依赖方向为什么重要
请求流向与代码依赖有关,但不是简单地把所有文件互相导入。这个结构建议遵守以下方向:
main.py是组装入口,可以导入 API 总路由、中间件和必要的应用级配置。- 中间件由
main.py导入和注册,不反向导入main.py,也不依赖具体路由函数或业务用例。 - API 层可以导入 Schema、Service,以及
db.session提供的请求依赖。 api/deps.py可以使用core.security、Service 或数据库依赖,但不要导入挂载它的router.py,避免形成router → deps → router循环。- Service 可以使用 ORM Model、传入的 Session、
core.security和真正通用的工具函数,不依赖具体路由函数。 db、schemas、core、utils等较低层模块不反向导入 API、Service 或中间件;数据库连接代码可以读取配置,但配置模块不应导入业务路由。- 另一个业务需要用户能力时,调用用户 Service 或明确的领域接口,不要调用用户的 HTTP 路由函数。
这些约束是为了避免 models → router → models 之类的循环,也让替换 HTTP 入口或测试业务逻辑时不必启动整个 Web 应用。__init__.py 中大量互相导入,往往会重新制造已经通过目录拆分避免的循环依赖。
目录什么时候应该调整
结构应该随真实复杂度演进,而不是永远不变:
- 很小的练习项目只有几个接口时,可以先使用少量文件,不必创建一整套空目录;没有实际中间件或通用工具时,也不必预先创建
middleware/、utils/。 models.py、Schema 或 Service 单文件过大时,可以拆成同名包下的多个业务文件。- 用户、商品、订单各自包含完整路由、模型和业务规则时,可以改为按业务包组织,例如把用户的 router、schemas、service、models 放在一个用户包内。
- 按业务包组织后,不必再维护一套内容重复的全局
schemas/、services/;共享基础设施仍可保留在core/、db/。 - 目录模块化不等于必须拆微服务。只有进程边界、独立部署等需求真实存在时,才需要承担网络调用和分布式事务等成本。
判断结构是否合适,可以看两个实际问题:新人能否快速找到某段职责,修改一个常见需求时是否需要无规律地跨很多文件。如果答案是否定的,就应调整结构,而不是继续增加目录层数。
配置集中管理
安装 uv add pydantic-settings,app/core/config.py:
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", extra="ignore",
)
app_name: str = "学习项目"
debug: bool = False
database_url: str = "sqlite:///./study.db"
@lru_cache
def get_settings():
return Settings()BaseSettings 负责把环境变量转换成声明的类型并校验,get_settings() 则给其他模块提供统一入口。@lru_cache 让同一进程复用一个 Settings 实例,避免每次访问都重新读取和解析 .env;如果测试在运行中修改了环境变量,要注意已有缓存不会自动变化,应清理缓存或为测试构造独立配置。
这里 SQLite 只是结构示例的默认值,不替代第 20 篇 MySQL 配置。.env 路径相对于工作目录;统一从项目根目录启动。真实密钥使用必填字段或外部密钥系统,不提供可误用于生产的固定默认密钥。
生命周期管理共享资源
共享 HTTP 客户端、连接池适合在应用 lifespan 初始化并在退出时关闭。每请求数据库 Session 则由依赖管理,不能把一个 Session 放到 app.state 让所有请求共享。
迁移脚本放 migrations,离线导入放 scripts。不要在多 worker 的模块导入时自动跑迁移或百万条初始化,多个进程可能同时执行。
版本控制与运行约定
.gitignore 至少忽略 .venv/、__pycache__/、.env、临时上传和测试缓存。.env.example 只列变量名与无敏感示例;依赖声明和锁文件进入版本控制。
启动入口统一为 uv run uvicorn app.main:app --reload。测试、迁移、部署也在项目根目录执行,减少随意修改 sys.path 带来的隐性依赖。
练习:画出各目录依赖方向,检查是否出现 models → router → models 或 router → deps → router 循环;将一项配置从散落常量迁入 Settings;把入口文件中的请求编号中间件移到 app/middleware/ 并显式注册;分别判断“获取当前用户、密码哈希、邮箱查重、折叠空白”应放在 api/deps.py、core/security.py、Service 还是 utils/。