用 FastAPI + SQLAlchemy 从零手搓一个博客系统:一套能落地、能讲清楚的 RESTful 后端

发布时间:2026/10/31 2:03:17

用 FastAPI + SQLAlchemy 从零手搓一个博客系统:一套能落地、能讲清楚的 RESTful 后端 面向想快速上手 Python 后端的中级开发者。读完你会发现一个“能跑、能扩、能讲明白”的后端骨架其实就那么几块拼图。开篇为什么我选了 FastAPI而不是 NestJS先说结论免得你纠结FastAPI上手快、类型友好、自带 Swagger 文档适合把 AI 能力快速包装成 Web API这也是它在 AI 圈火爆的原因。NestJS企业级、分层严谨、依赖注入一整套适合复杂、长期演进的业务系统。说白了就是——简单场景用 FastAPI 快刀斩乱麻复杂场景用 NestJS 稳扎稳打。这篇博客的场景是“把一套 CRUD 博客系统讲透”所以 FastAPI 是更合适的刀。技术栈选型很朴素角色选型理由Web 服务FastAPI Uvicorn异步、自带文档、8000 端口数据库SQLite单.db文件随程序发布不用像 MySQL/Postgres 那样独立部署ORMSQLAlchemy 2.0对象关系映射写对象就等于写 SQL密码加密bcrypt永不存明文密码一、先看目录分层架构就是后端的心智地图一个项目的目录结构往往比任何注释都更能说明“作者在想什么”。这套代码的分层非常清晰python/fastapi/ ├── main.py # 单点入口 路由注册 ├── database.py # 引擎、会话、Base、get_dbORM 基础设施 ├── models.py # 数据表 → 对象ORM 实体 ├── schemas.py # 入参/出参的约束Pydantic ├── crud.py # Service 层所有脏活累活的归宿 └── routers/ ├── __init__.py # 把一个目录变成包关键的小文件 ├── users.py # 用户模块路由 └── posts.py # 文章模块路由记住这条数据流后面每一层都对得上号HTTP 请求 → 路由(routers) → Schema 校验 → CRUD/Service → ORM → 数据库 ↑ ↓ 响应模型 ←──────────────────────────────────── 数据对象金句分层的本质是把“容易变的东西”和“不容易变的东西”隔离开。路由会变、校验规则会变、数据库操作也会变但把它们各放一层改哪块就不用牵一发动全身。这里有个容易被新手忽略的细节routers/__init__.py即使只写一行注释也必须存在——它把一个普通文件夹升级成能import的 Python 包。二、ORM 三剑客engine / session / Basedatabase.py是整个数据层的根只干三件事建引擎、建会话工厂、建 Base。别看代码短全是知识点from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, DeclarativeBase DATABASE_URL sqlite:///./blog.db # 轻量文本关系型数据库 # echoTrue 会打印出真实执行的 SQL调试神器上线务必关掉 engine create_engine(DATABASE_URL, echoTrue) # 会话工厂autoflush/autocommit 都关掉把控制权牢牢握在自己手里 SessionLocal sessionmaker(bindengine, autoflushFalse, autocommitFalse) class Base(DeclarativeBase): pass三个角色的分工一句话记住engine数据库引擎驱动 连接池是“通往数据库的那口井”。SessionLocal会话工厂。每次请求开一个 session用完就关就像每次办事拿一张临时通行证。Base所有 ORM 模型的父类。继承它一个普通类就“升格”成数据库表的映射。autoflushFalse不是随便写的——它意味着你手动干活commit之前ORM 不会偷偷帮你把中间状态刷进库里。这在“新增后想拿到自增 ID”这类场景里既能保证数据正确又能避免多余的 SQL是性能和安全之间的平衡点。踩坑预警create_engine在顶层sqlalchemy而sessionmaker、DeclarativeBase在sqlalchemy.orm。别想当然一行 import 全包我就在这儿栽过详见后文踩坑实录。三、get_dbyield 是 FastAPI 依赖注入的灵魂数据库会话怎么在请求间安全地开关FastAPI 用依赖注入 生成器给出了标准答案def get_db(): db SessionLocal() try: yield db # 把会话交给路由函数使用函数在此暂停 finally: db.close() # 请求结束无论成功还是报错都执行释放连接yield之前是“准备工作”yield之后finally是“善后工作”。FastAPI 会在请求进入时执行前半段请求结束时执行后半段——哪怕中间抛了异常会话也一定会被关闭绝不会出现连接泄漏。这种设计的美妙之处在于路由函数只需要声明db: Session Depends(get_db)就自动拿到一个“随取随用、用完即还”的会话完全不用操心生命周期。好的框架不会替你写业务但会替你处理好“开门”和“关门”这种最容易忘的事。四、models.py用 Mapped 把表写成类SQLAlchemy 2.0 最大的变化是全面走向类型注解风格。看这套用户表的写法干净得像在读自然语言class User(Base): __tablename__ users id: Mapped[int] mapped_column(primary_keyTrue, autoincrementTrue) username: Mapped[str] mapped_column(String(50), uniqueTrue, indexTrue) email: Mapped[str] mapped_column(String(100), uniqueTrue, indexTrue) hashed_password: Mapped[str] mapped_column(String(200)) is_active: Mapped[bool] mapped_column(defaultTrue) created_at: Mapped[datetime] mapped_column(DateTime, server_defaultfunc.now()) posts: Mapped[list[Post]] relationship( back_populatesauthor, cascadeall, delete-orphan )几个值得单独拎出来讲的点Mapped[类型]mapped_column(...)左边是 Python 类型右边是列约束IDE 能补全、类型检查能报警告别“字段写错运行时才炸”。indexTrue给username、email建索引。这两个字段天天查、还要求唯一索引是刚需。server_defaultfunc.now()时间由数据库自己生成不靠应用层传省心又统一。relationshipback_populates用户和文章的一对多关系双向都能导航。cascadeall, delete-orphan删用户时他名下文章一并清理不留孤儿数据。文章表用外键把归属关系钉死class Post(Base): __tablename__ posts id: Mapped[int] mapped_column(primary_keyTrue, autoincrementTrue) title: Mapped[str] mapped_column(String(200)) content: Mapped[str] mapped_column(String(5000)) published: Mapped[bool] mapped_column(defaultFalse) ... user_id: Mapped[int] mapped_column(ForeignKey(users.id), indexTrue) author: Mapped[User] relationship(back_populatesposts)user_id是那根“藤”author是顺着藤摸到的“瓜”。ORM 帮你把JOIN这种有点枯燥的东西翻译成了post.author.username这么直觉的一行代码。五、schemas.pyPydantic 是入参出参的“双保险”这里有个新手常混淆的概念必须先讲清楚models 是“数据库长什么样”schemas 是“接口收什么、发什么”。两者职责完全不同。class UserCreate(BaseModel): username: str Field(min_length3, max_length50, examples[zhangsan]) email: EmailStr Field(examples[zhangexample.com]) # 自带邮箱格式校验 password: str Field(min_length8, max_length100) # 明文密码仅用于入参 class UserResponse(BaseModel): model_config ConfigDict(from_attributesTrue) # 允许从 ORM 对象读取 id: int username: str email: str is_active: bool created_at: datetime关键点一入参 schema 和出参 schema 要分开。UserCreate里有passwordUserResponse里就绝不能有——response_model会像一个严格的过滤器就算你不小心把hashed_password塞进返回对象也不会泄漏给前端。关键点二ConfigDict(from_attributesTrue)是“ORM 对象 → JSON”的翻译开关。有了它直接return db_user就行Pydantic 会自动把对象的属性读成字段。更新场景用“全可选”的 schema配合exclude_unset实现只改传了的字段下面 CRUD 层会用到class UserUpdate(BaseModel): username: str | None Field(defaultNone, min_length3, max_length50) email: EmailStr | None None password: str | None Field(defaultNone, min_length8, max_length100) is_active: bool | None None还有一个很实用的技巧——嵌套响应模型。文章列表要带上作者信息不必写一个扁平的大对象直接嵌套class PostWithAuthor(BaseModel): model_config ConfigDict(from_attributesTrue) id: int title: str published: bool created_at: datetime author: UserResponse # 嵌套另一个 Pydantic 模型自动展开作者字段金句Schema 不是束缚它是你和前端之间的“契约”。写清楚了契约前端不用猜、后端不怕改。六、crud.py把所有脏活关进“小黑屋”路由层要保持清爽脏活累活都该沉到 service 层。crud.py就是这间小黑屋——所有select、commit、refresh都在这儿。密码加密永远别存明文def create_user(db: Session, user_in: UserCreate) - User: hashed bcrypt.hashpw( user_in.password.encode(utf-8), bcrypt.gensalt() ).decode(utf-8) db_user User(usernameuser_in.username, emailuser_in.email, hashed_passwordhashed) # 注意字段叫 hashed_password db.add(db_user) # 等价于 INSERT db.commit() db.refresh(db_user) # 把自增 id、数据库生成的时间回填到对象上 return db_userdb.refresh这一步千万别省——commit之后数据库里自动生成的主键、created_at这些值需要refresh才能同步回你的db_user对象否则返回给前端的可能是个残缺对象。查询SQLAlchemy 2.0 的 select 风格def get_user_by_email(db: Session, email: str) - User | None: stmt select(User).where(User.email email) return db.scalars(stmt).first() def get_users(db: Session, skip: int 0, limit: int 100) - list[User]: stmt select(User).offset(skip).limit(limit) # 分页 offset limit return db.scalars(stmt).all()select(...).where(...)不是立即执行的 SQL而是一个“SQL 描述对象”只有db.scalars()去取结果时才真正打库。这种“先描述、后执行”的模式让查询的拼装和组合变得非常灵活。局部更新exclude_unset 是精髓def update_post(db: Session, post: Post, post_in: PostUpdate) - Post: update_data post_in.model_dump(exclude_unsetTrue) # 只拿“用户真正传了”的字段 for field, value in update_data.items(): setattr(post, field, value) # 反射式赋值 db.commit() db.refresh(post) return postexclude_unsetTrue过滤掉了“没传的字段”于是前端只想改标题就只发标题其余字段原封不动。这就是 PATCH 语义的正确打开方式比“全量覆盖”安全太多了。一个顺手的小提醒我在代码里发现delete_post被写了两遍第 99 行和第 104 行重复定义——虽然 Python 后者会覆盖前者、不影响运行但这类“复制粘贴残留”是后期维护的隐形炸弹。写完记得扫一眼让代码干干净净。七、main.py单点入口 建表 路由注册入口文件短小精悍但每一步都有讲究from database import engine, Base from routers import users, posts import models # 必须 import否则模型没被注册 Base.metadata.create_all(bindengine) # 根据所有模型类自动建表 app FastAPI(titleBlog API, descriptionFastAPI SQLAlchemy 博客系统, version1.0.0) app.get(/) def root(): return {message: Blog API, docs: /docs} app.include_router(users.router) # 用户模块 app.include_router(posts.router)两个隐藏彩蛋import models不能删。create_all只认识“已经被导入、已经注册到 Base 元数据里”的模型类。少了这行表会“凭空消失”——一个非常隐蔽的坑。include_router让模块化落地。每个业务模块自带prefix和tagsmain.py只负责“组装”真正做到“高内聚、低耦合”。八、路由层RESTful 响应模型 状态码以用户模块为例一套标准的 CRUD RESTful 接口每个细节都值得学router APIRouter(prefix/users, tags[用户管理]) router.post(/, response_modelUserResponse, status_codestatus.HTTP_201_CREATED) def create_user(user_in: UserCreate, db: Session Depends(get_db)): existing crud.get_user_by_email(db, user_in.email) if existing: raise HTTPException(status_codestatus.HTTP_400_BAD_REQUEST, detail该邮箱已经注册) return crud.create_user(db, user_in) router.get(/, response_modellist[UserResponse]) def read_users(skip: int 0, limit: int 100, db: Session Depends(get_db)): return crud.get_users(db, skipskip, limitlimit) router.get(/{user_id}, response_modelUserResponse) def read_user(user_id: int, db: Session Depends(get_db)): user crud.get_user(db, user_id) if not user: raise HTTPException(status_codestatus.HTTP_404_NOT_FOUND, detail用户不存在) return user router.delete(/{user_id}, status_codestatus.HTTP_204_NO_CONTENT) def delete_user(user_id: int, db: Session Depends(get_db)): user crud.get_user(db, user_id) if not user: raise HTTPException(status_codestatus.HTTP_404_NOT_FOUND, detail用户不存在) crud.delete_user(db, user)几个必须掌握的约定response_model是出参的“海关”字段按它裁剪、类型按它校验。status_code精准表达语义。状态码用对了前端少写一半 if状态码场景200 OK查询成功201 Created新建成功POST204 No Content更新/删除成功无返回体400 Bad Request参数/业务校验失败如邮箱已注册404 Not Found资源不存在{user_id}路径参数会被自动解析成int因为类型注解写了int传个abc进来 FastAPI 直接给你 422连手动校验都省了。文章模块posts.py还演示了一个漂亮的“归属校验”模式——创建文章前必须确认用户存在router.post(/, response_modelPostResponse, status_codestatus.HTTP_201_CREATED) def create_post(user_id: int, post_in: PostCreate, db: Session Depends(get_db)): user crud.get_user(db, user_id) if not user: raise HTTPException(status_codestatus.HTTP_404_NOT_FOUND, detail用户不存在) return crud.create_post(db, post_in, user_iduser_id)以及一个很优雅的路由设计GET /posts/user/{user_id}拿某个用户的全部文章路径语义一目了然。金句RESTful 的精髓不是“路径好看”而是“让 URL 自己会说话”。看到/posts/user/3不用查文档就知道它在干嘛。九、踩坑实录我在这套系统上栽过的两个坑理论讲完来点血泪。这两个坑希望能帮你少熬两个夜。坑一NameError: name sessionmaker is not defined一开始database.py只写了from sqlalchemy import create_engine # 只有这一行结果一启动就炸sessionmaker找不到。原因很单纯——create_engine在顶层sqlalchemy而sessionmaker和DeclarativeBase在sqlalchemy.ormfrom sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, DeclarativeBase # 补上这一行教训SQLAlchemy 2.0 的导入路径不要凭记忆写两个命名空间容易混。报错NameError: name xxx is not defined十有八九就是“以为导入了其实没导入”。坑二满心期待 GET却等来 405 Method Not Allowed这个坑更阴间。代码里明明写了router.get(/)可一请求就是GET /users/ HTTP/1.1 405 Method Not Allowed排查半天才发现真相编辑器缓冲区里有 GET 路由但磁盘上的文件还停在只有 POST 的旧版本。Uvicorn 从磁盘加载代码它跑的根本不是你“心里想的那版”。按一下CtrlS保存--reload一刷新GET 立刻恢复正常。教训一句话点破金句你调试的永远是磁盘上的代码而不是你脑子里以为的那一版。改完不保存一切白搭。这两个坑叠加起来其实指向同一件事——报错信息是朋友不是敌人。NameError告诉你缺导入405告诉你方法没注册顺着线索走答案都在里面。十、跑起来三条命令一个文档页# 装依赖 pip install fastapi uvicorn sqlalchemy pydantic[email] bcrypt # 启动--reload 改代码自动重载开发期必备 uvicorn main:app --reload启动后浏览器打开http://127.0.0.1:8000/docs你会得到一份自动生成、可直接点着测的 Swagger 文档——这正是 FastAPI 最圈粉的地方代码即文档改完即生效。所有 schema 里的examples、min_length、EmailStr校验全都直观展示在这儿。验证一遍完整链路POST /users/建个用户 → 返回 201密码已被 bcrypt 加密存储GET /users/拿到列表 → 里面只有公开字段没有hashed_passwordPOST /posts/?user_id1给他发篇文章GET /posts/user/1拿回他名下所有文章。一条从“用户”到“文章”、带外键归属和嵌套作者信息的完整业务链就通了。结语后端骨架的本质回过头看这套系统没有一行炫技代码但每一层都各就各位database管连接models管表schemas管契约crud管逻辑routers管接口main管组装。你要的“能跑、能扩、能讲明白”就藏在这六个文件的边界里。最后送你一句框架负责规范和兜底你负责把业务讲清楚。当你能把这套骨架完整复述给别人听你就已经不只是“会用 FastAPI”而是“懂后端”了。
延伸阅读

更多相关文章

2026/10/31 2:03:17

补单的纸箱为什么容易和首批大货出现色差、尺寸偏差?

补单的纸箱为什么容易和首批大货出现色差、尺寸偏差?一、补单的一致性问题纸箱补单时,常出现和首批大货颜色不一致、尺寸有偏差的情况,并非厂家偷工减料,而是生产过程中多个变量发生了变化,导致两次生产的结果出现差异…

2026/10/31 1:52:52

OpenTelemetry 实战:打通日志、指标和链路追踪

目录 一、可观测性的困境与破局 (一)三套系统各自为战的排障之痛 (二)从监控到可观测性的演进 (三)OpenTelemetry 的定位与价值 二、核心概念与数据模型 (一)Trace、Span 与上…

2026/10/31 4:03:23

LLM+Agent驱动的材料智能设计:从范式迁移走向科研闭环

1. 材料设计圈正在发生的“静默革命”:不是谁在用AI,而是谁在用对的方式用AI最近帮一个做钙钛矿光伏材料的博士朋友看实验方案,他发来一份刚被《Advanced Materials》拒稿的修改稿。我扫了一眼补充材料里的计算流程图——还是传统的DFT参数扫…

2026/10/31 4:03:23

AI日报:多智能体协作、AI编程与内容生成工业化新趋势

今天是2026年9月21日,这份AI资讯日报整理了近期值得关注的行业动态和实操方向。今天的核心关键词集中在AI Agent、多智能体协作、AI大模型应用范式、AI编程与测试开发、AI短剧工业化,以及一批内容生成工具和垂直场景落地案例上。无论你是技术研发、内容创…

2026/10/31 4:03:23

数字图像处理入门:像素、采样量化、通道与数据类型

数字图像处理这六个字,很多人是从调包开始的:读图、缩放、模糊、存盘,跑通第一段代码时感觉已经入门了。可真要被人问一句“你说的这张图,到底是什么”,不少人就卡壳了:它是一张看得见的画面,还…

2026/10/31 4:03:23

YOLO全系列训练与模型转换工具:一站式解决方案

1. 项目概述:一站式YOLO训练与转换解决方案"懒人精灵YOLO全系列训练与模型转换工具"是面向计算机视觉开发者的一站式效率工具包,支持从YOLOv5到最新v12的全系列版本。这个工具最核心的价值在于将原本需要手动配置的环境、复杂的训练参数调整、…

2026/10/31 4:03:23

数字图像基础:像素数组、通道顺序与数值范围排查

同样一张图,在自己的脚本里读出来保存后颜色发灰、在另一个软件里打开又恢复正常,或者调试时发现本该是二维矩阵的灰度图居然是三维数组——这类问题我遇到过不止一次,追到根子上,几乎都是对"数字图像到底是什么"这件事…

2026/10/31 3:58:23

MySQL实战45讲:从原理到优化的深度解析

1. MySQL实战45讲专栏概览这个由前腾讯云数据库负责人丁奇(网名)主讲的专栏,堪称MySQL领域的"九阴真经"。不同于市面上那些零散的知识点堆砌,这个专栏用45讲的篇幅,通过36个真实生产环境中的痛点问题&#x…

2026/10/30 20:55:10

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/30 20:55:10

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/30 20:55:09

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/31 0:02:41

猫狗目标检测数据集:VOC/COCO/YOLO三格式一体化实战包

简介:本资源是一套面向计算机视觉初学者与YOLO目标检测实践者的猫狗图像数据集及配套开发工具包,专为快速上手目标检测模型训练而设计。资源包含1000张真实场景高清猫狗图片,标注质量高,同时提供VOC(XML)、…

2026/10/31 0:02:41

HDU2017多校标程复现:算法竞赛工程化真题沙盒

简介:本资源为2017年HDU多校联合训练第一场官方配套资料,面向ACM程序设计竞赛备赛学生、算法爱好者及高校编程集训队成员,旨在提供权威解题参考与完整测试验证环境。压缩包共40个文件,含15份C标程(覆盖全部赛题&#x…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑