【Web全栈进阶】FastAPI工程化:APIRouter拆分 + 配置 + 依赖注入

之前的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仓库地址】

相关推荐
Shirley~~1 小时前
Three.js的基础概念
前端·3d
deli0071 小时前
黏菌没有大脑,3000 个 Physarum 粒子跑 12973 步自己铺出了迷宫最短路
前端
凌云若寒1 小时前
BarTender提示#807错误:无法在拥有其他许可证的Licensing Service上激活节点锁定的 Professional 版许可证 的解决办法
运维·服务器·前端·学习·软件需求
嘟嘟同学和妮妮同学1 小时前
用原生 JS 做了一个中国历史帝皇梳理的可视化站(7 朝 77 帝 + Leaflet 地图)
前端
写后端的胖头鱼1 小时前
Redis 的 RDB 和 AOF
java·数据库·redis
黄权光1 小时前
【微信小程序】uni-app + Vue3 + Vite 的小程序项目实现「进入指定范围才能打卡」的考勤功能
前端
cxoptics1 小时前
LBO 激光损伤阈值 LIDT 典型数值?如何避免晶体打坏?
java
西柚小萌新1 小时前
【LLM&&AI应用开发 八股文】--4.2.Agent智能体(中)
前端·javascript·react.js
逐米时代2 小时前
远程运维诊断减少到场率
java·服务器·前端