用 FastAPI 搭一个新闻项目,router / crud / model / schema 应该怎么分层
2026/6/21 18:39:54 网站建设 项目流程

学完路由、参数、响应、依赖注入、ORM 之后,很多人会遇到第二个问题。

单个知识点都会了,但一到项目里,就不知道文件该怎么放。

新闻接口写哪里?

SQL 查询写哪里?

Pydantic 模型写哪里?

数据库表结构写哪里?

统一响应、异常处理、认证工具又写哪里?

如果所有代码都塞进main.py,项目很快就会变成一坨。

这篇我们用课程里的 AI 掘金头条项目,把 FastAPI 项目分层讲清楚。

核心思路很简单:

main.py只做装配,router管 HTTP,crud管数据库操作,model管表结构,schema管输入输出形状,utils放横切工具。

本篇准备

这一篇不引入新的依赖,沿用前面已经准备好的 FastAPI + SQLAlchemy 环境:

pipinstallfastapi uvicorn sqlalchemy aiomysql

本篇重点不是多装一个包,而是看清楚文件边界:入口、路由、数据访问、ORM 模型、请求响应模型分别应该放在哪里。

1. 为什么不能把所有代码都写进 main.py

刚开始学 FastAPI,这样写没问题:

fromfastapiimportFastAPI app=FastAPI()@app.get("/news/list")asyncdefget_news_list():return[]

但真实项目里,一个新闻系统至少有这些功能:

  • 新闻分类
  • 新闻列表
  • 新闻详情
  • 用户注册
  • 用户登录
  • 获取用户信息
  • 收藏新闻
  • 浏览历史
  • Redis 缓存
  • 异常处理
  • 跨域配置

如果全部堆在main.py,你会得到一个几千行的入口文件。

这类代码最可怕的地方不是长,而是边界消失。

HTTP 参数、数据库查询、业务规则、响应格式、异常处理全混在一起,后面想改一个接口都不知道会影响哪里。

所以项目必须分层。

2. AI 掘金头条的后端结构

课程最终项目的后端结构大概是这样:

toutiao_backend/ ├── main.py ├── config/ │ ├── db_conf.py │ └── cache_conf.py ├── models/ │ ├── users.py │ ├── news.py │ ├── favorite.py │ └── history.py ├── schemas/ │ ├── base.py │ ├── users.py │ ├── news.py │ ├── favorite.py │ └── history.py ├── crud/ │ ├── news.py │ ├── news_cache.py │ ├── users.py │ ├── favorite.py │ └── history.py ├── routers/ │ ├── news.py │ ├── users.py │ ├── favorite.py │ └── history.py ├── cache/ │ └── news_cache.py └── utils/ ├── auth.py ├── security.py ├── response.py ├── exception.py └── exception_handlers.py

看起来文件不少,但逻辑很清楚。

main.py 应用入口

routers 路由层

crud 数据访问层

models ORM 模型

MySQL

schemas 请求/响应模型

utils 横切工具

cache 缓存封装

Redis

这张图比目录本身更重要。

读一个 FastAPI 项目时,不要先问有多少文件。

先问这些文件各自负责什么。

3. main.py,只做应用级装配

最终项目里的main.py做的事情很少。

大概是:

fromfastapiimportFastAPIfromfastapi.middleware.corsimportCORSMiddlewarefromroutersimportnews,users,favorite,historyfromutils.exception_handlersimportregister_exception_handlers app=FastAPI()register_exception_handlers(app)app.add_middleware(CORSMiddleware,allow_origins=["http://localhost:5173","http://127.0.0.1:5173",],allow_credentials=True,allow_methods=["*"],allow_headers=["*"],)app.include_router(news.router)app.include_router(users.router)app.include_router(favorite.router)app.include_router(history.router)

你会发现,main.py基本不写业务逻辑。

它只负责:

  • 创建 FastAPI 应用
  • 注册异常处理器
  • 注册 CORS 中间件
  • 挂载业务路由

这就是一个干净入口文件应该有的样子。

入口文件越干净,项目越容易维护。

4. routers,负责 HTTP 接口层

routers/news.py这类文件负责定义接口。

它关心的是:

  • URL 是什么
  • HTTP 方法是什么
  • 参数从哪里来
  • 需要哪些依赖
  • 调用哪个 CRUD 函数
  • 返回什么响应

简化后的新闻列表接口可能长这样:

fromfastapiimportAPIRouter,Depends,Queryfromsqlalchemy.ext.asyncioimportAsyncSessionfromconfig.db_confimportget_dbfromcrudimportnews router=APIRouter(prefix="/api/news",tags=["新闻"])@router.get("/list")asyncdefget_news_list(category_id:int|None=Query(None,alias="categoryId"),page:int=Query(1,ge=1),page_size:int=Query(10,ge=1,le=100,alias="pageSize"),db:AsyncSession=Depends(get_db)):returnawaitnews.get_news_list(db=db,category_id=category_id,page=page,page_size=page_size)

这里有个边界很重要。

router 可以处理 HTTP 参数,但不应该堆复杂 SQL。

如果 router 里全是select(...)join(...)where(...),说明数据访问逻辑已经漏到 HTTP 层了。

5. crud,负责数据库操作

crud/news.py才是写数据库查询的地方。

示例:

fromsqlalchemyimportselect,funcfromsqlalchemy.ext.asyncioimportAsyncSessionfrommodels.newsimportNewsasyncdefget_news_list(db:AsyncSession,category_id:int|None,page:int,page_size:int):stmt=select(News)ifcategory_id:stmt=stmt.where(News.category_id==category_id)total_result=awaitdb.execute(select(func.count()).select_from(stmt.subquery()))total=total_result.scalar()offset=(page-1)*page_size result=awaitdb.execute(stmt.offset(offset).limit(page_size))return{"list":result.scalars().all(),"total":total,"hasMore":total>page*page_size}

crud 层关心的是数据库。

它不应该关心:

  • 请求头是什么
  • HTTP 状态码怎么返回
  • 前端路由叫什么

这能让数据访问逻辑更容易复用。

比如同一个get_news_list,未来可能被接口调用,也可能被后台任务调用。

6. models,负责数据库表结构

models/news.py表达的是表结构。

简化版:

fromsqlalchemyimportString,Text,Integer,DateTime,ForeignKeyfromsqlalchemy.ormimportMapped,mapped_columnclassNews(Base):__tablename__="news"id:Mapped[int]=mapped_column(Integer,primary_key=True)title:Mapped[str]=mapped_column(String(255))description:Mapped[str|None]=mapped_column(String(500))content:Mapped[str]=mapped_column(Text)category_id:Mapped[int]=mapped_column(ForeignKey("news_category.id"))views:Mapped[int]=mapped_column(Integer,default=0)

model 层不写接口逻辑。

它只回答一个问题:

数据库里这张表长什么样。

7. schemas,负责请求和响应形状

数据库字段不等于 API 字段。

这句话非常重要。

数据库里可能叫publish_time,前端可能希望拿到publishTime

数据库里可能有password,接口绝不能返回。

这时就需要 schema。

fromdatetimeimportdatetimefrompydanticimportBaseModel,ConfigDictclassNewsItemResponse(BaseModel):model_config=ConfigDict(from_attributes=True)id:inttitle:strdescription:str|Noneimage:str|Noneauthor:str|Noneviews:intpublish_time:datetime

Pydantic v2 里,ConfigDict(from_attributes=True)可以让模型从 ORM 对象属性里读取数据。

这样你就能把 SQLAlchemy 对象转成响应模型。

schema 层的价值是定义 API 契约。

只要契约稳定,数据库内部怎么调整,就不会轻易影响前端。

8. utils,放横切工具

项目里的utils包含:

文件作用
auth.py获取当前用户,Token 认证依赖
security.py密码哈希和校验
response.py统一成功响应
exception.py异常处理函数
exception_handlers.py注册异常处理器

这些逻辑不属于某一个具体业务模块。

它们横跨多个模块,所以放在utils比较合适。

但也要注意,utils很容易变成杂物间。

判断一个函数该不该进utils,可以问一句:

它是不是多个模块都需要,而且不依赖某个具体业务?

如果只是新闻模块内部用,就别放utils,放crud/news.py或新闻相关模块里更清楚。

9. 一次新闻列表请求的完整链路

现在把这些层串起来。

用户访问:

GET /api/news/list?categoryId=1&page=1&pageSize=10

项目内部大概这样跑:

MySQLmodels/news.pycrud/news.pyrouters/news.pymain.pyVue 前端MySQLmodels/news.pycrud/news.pyrouters/news.pymain.pyVue 前端GET /api/news/list路由匹配解析 Query 参数调用 get_news_list使用 News 模型构造查询执行 SQL返回数据返回列表和 totalJSON 响应

这条链路看懂了,项目结构就不再是死记硬背。

10. CORS 为什么放在 main.py

前端 Vite 默认可能跑在:

http://localhost:5173

后端 FastAPI 跑在:

http://127.0.0.1:8000

协议、域名、端口只要有一个不同,就是不同源。

浏览器会触发 CORS 限制。

FastAPI 通过CORSMiddleware解决:

fromfastapi.middleware.corsimportCORSMiddleware app.add_middleware(CORSMiddleware,allow_origins=["http://localhost:5173","http://127.0.0.1:5173",],allow_credentials=True,allow_methods=["*"],allow_headers=["*"],)

本地开发时,把 Vite 前端常用的两个地址写进去就够了。

正式上线时不要这么放。

应该明确指定前端域名,例如:

allow_origins=["https://your-frontend-domain.com"]

这是安全边界,不是格式问题。

尤其要注意,如果allow_credentials=True,就不要再用allow_origins=["*"]。浏览器携带 Cookie、认证头这类凭证时,跨域规则必须更明确。

11. 分层不是为了好看,是为了控制变化

项目分层的核心不是“看起来专业”。

而是控制变化范围。

变化应该主要影响哪里
URL 改了router
数据库查询改了crud
表字段改了model 和迁移脚本
API 返回字段改了schema
Token 认证规则改了utils/auth
响应格式改了utils/response

这就是分层的意义。

当变化来了,你知道去哪改,也知道不该碰哪里。

12. 小结

FastAPI 项目分层可以用这句话记:

main.py 装配应用 router 接 HTTP crud 查数据库 model 映射表 schema 定契约 utils 放通用工具

AI 掘金头条项目从 day03 开始做新闻模块,day04 加用户模块,day05 加收藏和历史,day06 加 Redis 缓存。

功能越来越多,但只要分层边界清楚,项目不会失控。

下一篇我们继续看用户模块。

注册、登录、Token、密码哈希、统一响应、全局异常处理,这些东西加上之后,一个 FastAPI 项目才真正开始像一个项目。

参考资料

  • FastAPI Bigger Applications
  • FastAPI CORS Middleware
  • Pydantic v2 Models

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询