FastAPI 分层架构:router、service、repository 怎么拆
更新说明:补充 FastAPI 工程分层实践。
系列:Python Web 工程实践
- Python Web 项目配置管理:环境变量、.env 和生产配置怎么拆
- Python Web 项目如何连接数据库:SQLAlchemy、Django ORM、迁移怎么选
- Alembic 数据库迁移入门:表结构变更怎么安全上线
- Django 数据库迁移实战:makemigrations 和 migrate 背后的坑
- FastAPI 项目测试指南:pytest、TestClient、依赖覆盖怎么用
- Django 测试入门:Model、View、API 测试怎么写
- FastAPI 分层架构:router、service、repository 怎么拆
- PostgreSQL 基础:索引、事务、慢查询怎么理解
FastAPI 很容易从一个文件开始。刚写几个接口时,把路由、参数校验、数据库查询和业务判断都放在一起,确实很快。但接口变多后,单个函数会越来越长,测试也越来越难写。
这时可以考虑把项目拆成 router、service、repository 三层。不是为了显得架构复杂,而是为了让每段代码只负责一件事。
一个常见目录结构
app/ main.py api/ routes/ articles.py services/ article_service.py repositories/ article_repository.py schemas/ article.py models/ article.py deps.py这个结构不是唯一答案,但足够应付大多数中小型项目。项目结构的整体思路可以先看 Python Web 项目结构怎么设计。
router 只处理 HTTP
router 层负责接收请求、声明参数、返回响应。它应该尽量薄。
from fastapi import APIRouter, Depends
from app.schemas.article import ArticleCreate, ArticleOutfrom app.services.article_service import ArticleService
router = APIRouter(prefix="/articles", tags=["articles"])
@router.post("/", response_model=ArticleOut)def create_article( payload: ArticleCreate, service: ArticleService = Depends(),): return service.create(payload)这里不直接写数据库查询,也不塞复杂业务规则。这样接口函数一眼能看出输入和输出。
service 放业务规则
service 层处理业务判断,比如标题是否重复、用户是否有权限、状态能不能流转。
from fastapi import HTTPException
from app.repositories.article_repository import ArticleRepositoryfrom app.schemas.article import ArticleCreate
class ArticleService: def __init__(self, repo: ArticleRepository = Depends()): self.repo = repo
def create(self, payload: ArticleCreate): if self.repo.exists_slug(payload.slug): raise HTTPException(status_code=409, detail="slug already exists")
return self.repo.create(payload)service 不应该关心 HTTP 路径,也不应该拼 SQL。它关心的是“这个动作在业务上是否成立”。
repository 负责数据访问
repository 层把数据库操作收起来。
from sqlmodel import Session, select
from app.models.article import Article
class ArticleRepository: def __init__(self, session: Session = Depends(get_session)): self.session = session
def exists_slug(self, slug: str) -> bool: statement = select(Article).where(Article.slug == slug) return self.session.exec(statement).first() is not None
def create(self, payload): article = Article(**payload.model_dump()) self.session.add(article) self.session.commit() self.session.refresh(article) return article这一层可以替换数据库实现,也方便在测试里换成假的 repository。
schema 和 model 不要混用
schema 是接口输入输出,model 是数据库结构。它们可以长得像,但职责不同。
from pydantic import BaseModel
class ArticleCreate(BaseModel): title: str slug: str
class ArticleOut(BaseModel): id: int title: str slug: str不要为了省事直接把数据库模型暴露给接口返回。后面字段一多,很容易把内部字段暴露出去。
测试会变简单
分层之后,service 可以单独测业务规则,router 可以测 HTTP 行为。
def test_create_rejects_duplicate_slug(): repo = FakeArticleRepository(existing_slugs={"hello"}) service = ArticleService(repo)
with pytest.raises(HTTPException): service.create(ArticleCreate(title="Hello", slug="hello"))接口测试可以继续参考 FastAPI 项目测试指南:pytest、TestClient、依赖覆盖怎么用。
不要过度拆分
不是每个接口都需要完整三层。如果只是健康检查,直接写 router 就够了。
@router.get("/health")def health(): return {"status": "ok"}判断是否需要拆分,看这几个信号:
- 一个路由函数超过几十行
- 同一段业务判断在多个接口重复
- 测试时不得不启动完整应用才能测业务规则
- 数据库查询散落在很多文件里
- 接口返回字段经常和数据库字段互相牵扯
出现这些问题,再拆分就很自然。
总结
FastAPI 分层的目标不是套模板,而是让变化有地方放。router 管 HTTP,service 管业务,repository 管数据访问,schema 管输入输出。
项目小的时候可以保持简单。等接口、测试和数据库逻辑开始增长,再逐步拆层。这样既不会过度设计,也不会让项目在后期变成一堆难维护的接口函数。