之前的FastAPI还活在单文件里:所有路由挤在一个main.py。
200行时没问题,500行时没人敢动------今天把它升级成"分模块的工程 ",用早报站的API实战三件套:APIRouter拆路由、Settings管配置、Depends依赖注入。
🎯 本篇产出:早报站FastAPI应用骨架------app/分层、两个路由模块、统一会话注入、配置文件外置。含代码约150行。
📌 太长不看版(给想快速上手的你)
| 项目信息 | 一句话说明 |
|---|---|
| 本篇目标 | FastAPI从单文件升级为分模块工程 |
| 代码行数 | ~150行(含注释) |
| 依赖 | fastapi + pydantic-settings(新增) |
| 核心功能 | APIRouter拆分 + Settings配置 + Depends依赖注入 |
| 跑起来的命令 | uvicorn app.main:app --reload |
| 核心知识点 | 分层结构、配置外置、依赖注入 |
| 做完你能得到 | 一套能维护、可扩展的FastAPI工程骨架 |
⚠️ 工程声明 :本篇先把FastAPI的"骨架与结构"立起来,响应模型(response_model)的完整规范留到后面联调篇统一设计------今天解决"结构",明天解决"规范"。
一、为什么需要"工程化":单文件的三宗罪
单文件API是这个画风:
python
# 所有路由、所有逻辑、所有配置挤在一个文件
@app.get("/todos") ...
@app.post("/todos") ...
@app.put("/todos/{id}") ...
三个问题会随着代码长大依次爆发:
| # | 问题 | 表现 |
|---|---|---|
| ① | 路由一多文件失控 | 找接口靠Ctrl+F |
| ② | 配置散落 | 连接串、密钥写在文件各处 |
| ③ | 重复代码 | 每个接口都写一遍"开会话、关会话" |
📌 工程化就是把这三件事制度化------这正是早报站从"玩具"走向"产品"必须跨过的一步。
二、新结构:app/层的标准布局
text
python_daily/
├── core/ # 数据层(第03、04篇已就位)
│ ├── db.py # 引擎与会话
│ └── models.py # ORM模型
├── app/ # API层(本篇新建)
│ ├── __init__.py
│ ├── main.py # 应用入口:创建app、挂载路由
│ ├── config.py # Settings:配置集中管理
│ ├── deps.py # 共享依赖(get_session等)
│ └── routers/ # 路由按资源分文件
│ ├── __init__.py
│ ├── articles.py
│ └── sources.py
└── .env # 本地配置(进.gitignore!)
🎯 分层逻辑一句话 :core是"数据怎么存",app是"接口怎么暴露",routers是"一个资源一个文件"。
三、第1步:Settings------配置只写一处
app/config.py:
python
"""app/config.py ------ 全局配置"""
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""配置项集中声明;环境变量与.env文件自动注入"""
database_url: str = "postgresql+psycopg://postgres:你的密码@localhost:5432/daily"
secret_key: str = "dev-only"
debug: bool = False
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
settings = Settings() # 全局唯一实例,其他地方import它
📌 三个细节
| # | 细节 | 说明 |
|---|---|---|
| ① | env_file=".env" |
自动读项目根的.env文件------.env必须进.gitignore(密钥不在代码里、不在仓库里,只在环境里) |
| ② | 环境变量自动匹配 | 字段database_url自动匹配环境变量DATABASE_URL(大小写不敏感),.env里写DATABASE_URL=xxx即生效 |
| ③ | 默认值=本地开发兜底 | 生产环境用环境变量覆盖,代码零改动 |
💡 以后任何接口要用配置,一行
from app.config import settings。
📄 .env示例(记得进.gitignore)
text
DATABASE_URL=postgresql+psycopg://postgres:你的密码@localhost:5432/daily
SECRET_KEY=你的随机密钥
DEBUG=false
四、第2步:共享依赖------session注入
每个接口都要数据库会话,但"开一个、用、关一个"不该重复写。FastAPI的依赖注入(Depends)把这件事制度化:
app/deps.py:
python
"""app/deps.py ------ 共享依赖"""
from core.db import Session
def get_session():
"""每个请求一个独立会话;请求结束自动关闭(with保证)"""
with Session() as session:
yield session
app/routers/articles.py:
python
"""app/routers/articles.py ------ 文章接口"""
from fastapi import APIRouter, Depends
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.deps import get_session
from core.models import Article
router = APIRouter(prefix="/articles", tags=["articles"])
@router.get("")
def list_articles(
session: Session = Depends(get_session), # 依赖注入:FastAPI自动调用get_session
limit: int = 10,
):
"""文章列表:最新在前"""
stmt = select(Article).order_by(Article.id.desc()).limit(limit)
return session.scalars(stmt).all()
📖 拆开看发生了什么
| # | 关键点 | 说明 |
|---|---|---|
| ① | Depends(get_session) |
声明"这个接口需要get_session提供的东西"------FastAPI收到请求时自动调用get_session,把yield出的session作为参数传进来,请求结束自动执行收尾 |
| ② | 好处一:接口代码干净 | 函数体里只有业务逻辑,没有样板 |
| ③ | 好处二:全局替换 | 将来测试时(第26篇),把get_session换成"测试库会话",所有接口自动切换,一行接口代码不用改 |
📌 依赖注入的第一个字面收益是整洁,深层收益是可替换。
🎯 再加一个业务型依赖练手------分页参数
app/routers/articles.py补充:
python
def get_page(page: int = 1, size: int = 10):
"""分页依赖:page从1开始,返回(offset, limit)"""
return (page - 1) * size, size
@router.get("/top")
def top_articles(
session: Session = Depends(get_session),
page: tuple[int, int] = Depends(get_page), # 依赖还能组合依赖
):
offset, limit = page
stmt = select(Article).order_by(Article.id.desc()).offset(offset).limit(limit)
return session.scalars(stmt).all()
💡 注意
page的类型注解是tuple[int, int],和get_page返回值对齐------类型注解要和实际返回一致(第七节④会讲这个坑)。
五、第3步:应用入口------把路由挂上去
app/main.py:
python
"""app/main.py ------ FastAPI应用入口"""
from fastapi import FastAPI
from app.routers import articles, sources
app = FastAPI(title="早报站 API", version="0.2.0")
app.include_router(articles.router)
app.include_router(sources.router)
app/routers/sources.py(订阅源接口,模式同articles):
python
from fastapi import APIRouter, Depends
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.deps import get_session
from core.models import Source
router = APIRouter(prefix="/sources", tags=["sources"])
@router.get("")
def list_sources(session: Session = Depends(get_session)):
return session.scalars(select(Source)).all()
✅ 运行(在项目根目录执行)
bash
uvicorn app.main:app --reload
打开/docs------见证结构化的第一个红利:
🎉 Swagger里接口按articles / sources分组显示,tags自动归类,接口多了也不乱。
六、验收清单
bash
1. uvicorn启动无报错,/docs里articles / sources两组接口
2. 浏览器访问/articles?limit=3 → 返回3条文章JSON
3. 访问/articles/top?page=2 → 翻页生效(offset正确)
4. 改.env的DATABASE_URL指向不存在的库 → 接口报可读错误 → 改回
5. git status确认.env不在仓库里
6. 全程无报错后提交Git
bash
git add .
git commit -m "FastAPI工程化:路由拆分 + Settings + 依赖注入"
七、常见报错:这6个,工程化改造的标配(重点!)
① ModuleNotFoundError: No module named 'pydantic_settings'
🔍 原因:pydantic-settings需要单独安装(FastAPI自带pydantic,不带settings)。
✅ 解法:
bash
pip install pydantic-settings
pip freeze > requirements.txt
② ModuleNotFoundError: No module named 'app'
🔍 原因:启动目录不对------不在项目根目录执行uvicorn。
✅ 解法 :cd python_daily再跑;cwd在sys.path里才有app包(一季第5篇的老规矩)。
③ /docs里接口地址变成/articles//或访问报307
🔍 原因 :prefix="/articles"加@router.get("/")拼出了双斜杠。
✅ 解法:
- 带prefix时路由写空串
@router.get(""); - 不带prefix时写
"/"。
📌 prefix与路径的拼接规则要记牢。
④ 访问接口报missing required argument: 'session'
🔍 原因 :函数签名写了session: Session但忘了= Depends(get_session)------FastAPI把它当成普通必填参数。
✅ 解法 :依赖注入的声明是Depends(...),不是类型注解本身。
📌 类型注解只是文档,Depends才是接线。
⑤ 返回的JSON里datetime报cannot encode datetime
🔍 原因:直接返回ORM对象时,FastAPI默认序列化不了datetime等复杂类型。
✅ 解法 :现在表里全是基础类型所以没炸;加了时间列就会遇到------响应模型(response_model)与序列化规范在后续统一处理。
📌 先记住:裸ORM对象返回是过渡态。
⑥ 改了.env配置,程序没反应
🔍 原因 :Settings在import时读一次.env------改完要重启uvicorn(--reload会自动重启,但改了.env有时不触发)。
✅ 解法 :手动重启;或确认.env在项目根、名字正确(无后缀,就叫.env)。
八、课后练习
| # | 练习 | 难度 | 提示 |
|---|---|---|---|
| 1 | 补齐sources接口:给订阅源加"按url模糊搜索" | ⭐⭐ | Source.url.contains(keyword) |
| 2 | 分页依赖升级 :给get_page加max_size=50上限,size传1000时自动截断 |
⭐⭐ | 依赖里的防御逻辑一次生效全接口受益 |
| 3 | 密钥搬家:把遗留的明文配置全部迁入Settings + .env | ⭐⭐ | 密钥军规正式落地 |
| 4(选做) | 新旧对照:用新结构重写待办API | ⭐⭐⭐ | 感受"单文件"与"工程"的差距 |
📦 配套代码
完整app/结构已上传Git(python_daily/):【gitee仓库地址】