文章列表
3 分钟阅读

FastAPI 分层架构:router、service、repository 怎么拆

更新说明:补充 FastAPI 工程分层实践。


系列:Python Web 工程实践

第 7 / 8 篇

  1. Python Web 项目配置管理:环境变量、.env 和生产配置怎么拆
  2. Python Web 项目如何连接数据库:SQLAlchemy、Django ORM、迁移怎么选
  3. Alembic 数据库迁移入门:表结构变更怎么安全上线
  4. Django 数据库迁移实战:makemigrations 和 migrate 背后的坑
  5. FastAPI 项目测试指南:pytest、TestClient、依赖覆盖怎么用
  6. Django 测试入门:Model、View、API 测试怎么写
  7. FastAPI 分层架构:router、service、repository 怎么拆
  8. 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 层负责接收请求、声明参数、返回响应。它应该尽量薄。

app/api/routes/articles.py
from fastapi import APIRouter, Depends
from app.schemas.article import ArticleCreate, ArticleOut
from 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 层处理业务判断,比如标题是否重复、用户是否有权限、状态能不能流转。

app/services/article_service.py
from fastapi import HTTPException
from app.repositories.article_repository import ArticleRepository
from 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 层把数据库操作收起来。

app/repositories/article_repository.py
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 是数据库结构。它们可以长得像,但职责不同。

app/schemas/article.py
from pydantic import BaseModel
class ArticleCreate(BaseModel):
title: str
slug: str
class ArticleOut(BaseModel):
id: int
title: str
slug: str

不要为了省事直接把数据库模型暴露给接口返回。后面字段一多,很容易把内部字段暴露出去。

测试会变简单

分层之后,service 可以单独测业务规则,router 可以测 HTTP 行为。

tests/test_article_service.py
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 管输入输出。

项目小的时候可以保持简单。等接口、测试和数据库逻辑开始增长,再逐步拆层。这样既不会过度设计,也不会让项目在后期变成一堆难维护的接口函数。