之前的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是这个画风:
# 所有路由、所有逻辑、所有配置挤在一个文件@app.get("/todos")...@app.post("/todos")...@app.put("/todos/{id}")...三个问题会随着代码长大依次爆发:
| # | 问题 | 表现 |
|---|---|---|
| ① | 路由一多文件失控 | 找接口靠Ctrl+F |
| ② | 配置散落 | 连接串、密钥写在文件各处 |
| ③ | 重复代码 | 每个接口都写一遍“开会话、关会话” |
📌工程化就是把这三件事制度化——这正是早报站从“玩具”走向“产品”必须跨过的一步。
二、新结构:app/层的标准布局
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:
"""app/config.py —— 全局配置"""frompydantic_settingsimportBaseSettings,SettingsConfigDictclassSettings(BaseSettings):"""配置项集中声明;环境变量与.env文件自动注入"""database_url:str="postgresql+psycopg://postgres:你的密码@localhost:5432/daily"secret_key:str="dev-only"debug:bool=Falsemodel_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)
DATABASE_URL=postgresql+psycopg://postgres:你的密码@localhost:5432/daily SECRET_KEY=你的随机密钥 DEBUG=false四、第2步:共享依赖——session注入
每个接口都要数据库会话,但“开一个、用、关一个”不该重复写。FastAPI的依赖注入(Depends)把这件事制度化:
app/deps.py:
"""app/deps.py —— 共享依赖"""fromcore.dbimportSessiondefget_session():"""每个请求一个独立会话;请求结束自动关闭(with保证)"""withSession()assession:yieldsessionapp/routers/articles.py:
"""app/routers/articles.py —— 文章接口"""fromfastapiimportAPIRouter,Dependsfromsqlalchemyimportselectfromsqlalchemy.ormimportSessionfromapp.depsimportget_sessionfromcore.modelsimportArticle router=APIRouter(prefix="/articles",tags=["articles"])@router.get("")deflist_articles(session:Session=Depends(get_session),# 依赖注入:FastAPI自动调用get_sessionlimit:int=10,):"""文章列表:最新在前"""stmt=select(Article).order_by(Article.id.desc()).limit(limit)returnsession.scalars(stmt).all()📖 拆开看发生了什么
| # | 关键点 | 说明 |
|---|---|---|
| ① | Depends(get_session) | 声明“这个接口需要get_session提供的东西”——FastAPI收到请求时自动调用get_session,把yield出的session作为参数传进来,请求结束自动执行收尾 |
| ② | 好处一:接口代码干净 | 函数体里只有业务逻辑,没有样板 |
| ③ | 好处二:全局替换 | 将来测试时(第26篇),把get_session换成“测试库会话”,所有接口自动切换,一行接口代码不用改 |
📌依赖注入的第一个字面收益是整洁,深层收益是可替换。
🎯 再加一个业务型依赖练手——分页参数
app/routers/articles.py补充:
defget_page(page:int=1,size:int=10):"""分页依赖:page从1开始,返回(offset, limit)"""return(page-1)*size,size@router.get("/top")deftop_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)returnsession.scalars(stmt).all()💡 注意
page的类型注解是tuple[int, int],和get_page返回值对齐——类型注解要和实际返回一致(第七节④会讲这个坑)。
五、第3步:应用入口——把路由挂上去
app/main.py:
"""app/main.py —— FastAPI应用入口"""fromfastapiimportFastAPIfromapp.routersimportarticles,sources app=FastAPI(title="早报站 API",version="0.2.0")app.include_router(articles.router)app.include_router(sources.router)app/routers/sources.py(订阅源接口,模式同articles):
fromfastapiimportAPIRouter,Dependsfromsqlalchemyimportselectfromsqlalchemy.ormimportSessionfromapp.depsimportget_sessionfromcore.modelsimportSource router=APIRouter(prefix="/sources",tags=["sources"])@router.get("")deflist_sources(session:Session=Depends(get_session)):returnsession.scalars(select(Source)).all()✅ 运行(在项目根目录执行)
uvicorn app.main:app--reload打开/docs——见证结构化的第一个红利:
🎉Swagger里接口按articles / sources分组显示,tags自动归类,接口多了也不乱。
六、验收清单
1. uvicorn启动无报错,/docs里articles / sources两组接口2. 浏览器访问/articles?limit=3→ 返回3条文章JSON3. 访问/articles/top?page=2→ 翻页生效(offset正确)4. 改.env的DATABASE_URL指向不存在的库 → 接口报可读错误 → 改回5.gitstatus确认.env不在仓库里6. 全程无报错后提交Gitgitadd.gitcommit-m"FastAPI工程化:路由拆分 + Settings + 依赖注入"七、常见报错:这6个,工程化改造的标配(重点!)
①ModuleNotFoundError: No module named 'pydantic_settings'
🔍 原因:pydantic-settings需要单独安装(FastAPI自带pydantic,不带settings)。
✅ 解法:
pipinstallpydantic-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仓库地址】