☕ 咖啡工坊 · FastAPI 22 章教程合集
本文件由 22 篇原始文档按章节顺序合并而成,原始文档已保留在 docs/ 目录下,可随时查阅。
目录
- [第 01 章 什么是 FastAPI](#第 01 章 什么是 FastAPI)
- [第 02 章 FastAPI 核心特性](#第 02 章 FastAPI 核心特性)
- [第 03 章 FastAPI 首个项目运行](#第 03 章 FastAPI 首个项目运行)
- [第 04 章 基础路由与请求方式](#第 04 章 基础路由与请求方式)
- [第 05 章 三大请求参数详解](#第 05 章 三大请求参数详解)
- [第 06 章 参数校验](#第 06 章 参数校验)
- [第 07 章 响应数据处理](#第 07 章 响应数据处理)
- [第 08 章 静态文件与模板渲染](#第 08 章 静态文件与模板渲染)
- [第 09 章 文件上传与下载](#第 09 章 文件上传与下载)
- [第 10 章 异常处理与全局异常捕获](#第 10 章 异常处理与全局异常捕获)
- [第 11 章 中间件与跨域](#第 11 章 中间件与跨域)
- [第 12 章 依赖注入](#第 12 章 依赖注入)
- [第 13 章 基础认证方式](#第 13 章 基础认证方式)
- [第 14 章 JWT 令牌认证](#第 14 章 JWT 令牌认证)
- [第 15 章 规范项目目录结构](#第 15 章 规范项目目录结构)
- [第 16 章 数据库的联动开发](#第 16 章 数据库的联动开发)
- [第 17 章 接口版本管理](#第 17 章 接口版本管理)
- [第 18 章 同步和异步接口](#第 18 章 同步和异步接口)
- [第 19 章 异步数据库和异步请求](#第 19 章 异步数据库和异步请求)
- [第 20 章 高并发注意要点](#第 20 章 高并发注意要点)
- [第 21 章 日志 / 测试 / 接口文档](#第 21 章 日志 / 测试 / 接口文档)
- [第 22 章 项目部署与上线](#第 22 章 项目部署与上线)
第 01 章 什么是 FastAPI
本章为概念章,不涉及可运行代码。读完后请直接进入第 02 章动手实践。
一、FastAPI 是什么
FastAPI 是一个用于构建 API(应用程序编程接口)的现代 Python Web 框架。它脱胎于 Starlette(处理 ASGI 网络层)与 Pydantic(数据校验层),站在两个优秀项目的肩膀上,因此天生具备:
| 维度 | 体现 |
|---|---|
| 速度 | 性能与 Node.js、Go 同级,是 Python Web 框架中最快的之一 |
| 开发效率 | 接近 Flask 的简洁,但功能远超 Flask |
| 类型安全 | 基于 Python 3.10+ 的类型提示 + Pydantic,请求 / 响应全自动校验 |
| 文档自动化 | 自动生成 OpenAPI 规范(Swagger / ReDoc),无需手写文档 |
| 异步友好 | 原生支持 async / await,与 asyncio 生态无缝衔接 |
二、FastAPI 与其它框架的关系

- Starlette:负责 HTTP 解析、路由、中间件、依赖注入,是 FastAPI 的"骨架"。
- Pydantic:负责数据校验、序列化、模型定义,是 FastAPI 的"血肉"。
- Uvicorn:基于 uvloop + httptools 的 ASGI 服务器,真正把代码跑起来。
也就是说,FastAPI 自身代码量不大,但它把社区最强的两个库粘合在一起,所以你写出来的应用又快又稳。
三、为什么选择 FastAPI
- 写起来像写注释:类型提示就是文档,编辑器能自动补全。
- 错误信息友好:校验失败时返回的 JSON 错误结构清晰,前端能直接渲染。
- 生态完整:OAuth2、JWT、SQL 建模、后台任务、WebSocket 一应俱全。
- 生产可用:从单文件 demo 到大型 SaaS 都能胜任,Netflix、Uber、Microsoft 都在用。
四、本教程的定位
| 维度 | 本教程选择 |
|---|---|
| Python 版本 | 3.14(使用最新的类型语法) |
| 包管理 | uv(极快的依赖解析与虚拟环境) |
| 代码风格 | 全程类型提示 + 行内注释,不留任何跳跃 |
| 业务主题 | 咖啡工坊(CoffeeCraft)在线运营平台 |
| 端口约定 | 第 N 章统一使用 880N 端口 |
读完本章你应该带着一个疑问:"它到底快在哪里?开发体验到底如何?",带着这个疑问进入第 02 章。
第 02 章 FastAPI 核心特性
本章为特性预览章,所有示例均可在第 03 章之后动手验证。
一、五大核心特性一览
| 编号 | 特性 | 一句话概括 |
|---|---|---|
| ① | 基于类型提示 | 用 Python 类型注解声明接口参数与返回值,编辑器可静态检查、自动补全 |
| ② | 自动数据校验 | 借助 Pydantic,请求体 / Query / Path 中的字段全部自动校验 |
| ③ | 自动生成文档 | 启动应用即得到 Swagger UI 与 ReDoc,无需手写文档 |
| ④ | 异步优先 | 路由处理函数支持 async def,与 asyncio 生态完美契合 |
| ⑤ | 依赖注入 | 通过 Depends() 组合可复用的逻辑(数据库连接、鉴权、子依赖链) |
下面我们逐条拆解。
二、特性 ①:类型即文档
python
from datetime import date
def create_order(customer: str, items: list[str], pickup_date: date) -> dict:
...
FastAPI 会把这段类型提示直接翻译成 OpenAPI 文档片段:
json
{
"customer": {"type": "string"},
"items": {"type": "array", "items": {"type": "string"}},
"pickup_date": {"type": "string", "format": "date"}
}
IDE 在你写代码时就已经知道 customer 必须是 str,items 是 list[str],连错误都会提前提醒。
三、特性 ②:Pydantic 自动校验
当你用 Pydantic 模型声明请求体:
python
from pydantic import BaseModel, Field
class OrderIn(BaseModel):
customer: str = Field(min_length=1, max_length=50)
cups: int = Field(gt=0, le=20)
客户端若发送 cups: 0,FastAPI 会自动返回 422 状态码 + 错误详情,你不需要写一行校验代码。
四、特性 ③:自动文档
启动应用后浏览器访问:
http://127.0.0.1:8803/docs→ Swagger UI(可调试)http://127.0.0.1:8803/redoc→ ReDoc(只读,更适合交付)
文档与代码始终同步,因为文档就是从代码"长出来的"。
五、特性 ④:async 优先
FastAPI 同时支持同步 (def) 与异步 (async def) 处理函数:
python
@app.get("/sync")
def sync_route(): # 阻塞型
return {"kind": "sync"}
@app.get("/async")
async def async_route(): # 协程型
return {"kind": "async"}
def适合调用阻塞型库(如部分老版数据库驱动)。async def适合 I/O 密集场景(HTTP、数据库、WebSocket),能释放事件循环。
第 18 章会专门对比二者性能差异。
六、特性 ⑤:依赖注入
python
from typing import Annotated
from fastapi import Depends
def get_menu_service():
return MenuService()
@app.get("/menu")
def list_menu(svc: Annotated[MenuService, Depends(get_menu_service)]):
return svc.all()
- 依赖可以是普通函数、类、生成器。
- 可嵌套:依赖里再依赖另一个依赖。
- 同一个请求内依赖只执行一次(缓存)。
七、把五大特性串成一条主线
text
类型提示 → Pydantic 校验 → OpenAPI 文档 → 异步执行 → 依赖复用
① ② ③ ④ ⑤
后续章节会按 ① → ⑤ 的顺序逐步展开,每一个特性都会成为下一章的"自然延伸"。第 03 章我们就从"启动第一个项目"开始。
第 03 章 FastAPI 首个项目运行
本章是动手的第一站,我们会启动一个最小化的 FastAPI 应用,并理解每个文件、每行代码的作用。
端口约定:本章使用 8803。
一、运行前准备
确保你已经在项目根目录执行过:
bash
uv sync # 同步依赖(生成 .venv)
uv add fastapi uvicorn # 已包含在 pyproject.toml,无需重复
本项目使用 uv 管理依赖与虚拟环境,无需手动
python -m venv。
二、最小化可运行代码
完整源码见 chapters/ch03_first_run/main.py。下面逐行拆解。
2.1 导入与创建实例
python
from fastapi import FastAPI # 导入 FastAPI 类
app = FastAPI() # 创建一个 ASGI 应用实例
FastAPI()实例就是整个 Web 应用的"根"。- 同一个进程内可以创建多个实例(多应用隔离时很有用),但本教程统一一个实例。
2.2 注册第一个路由
python
@app.get("/") # 把下面的函数注册到 GET /
def root() -> dict[str, str]: # 函数返回 dict,FastAPI 会自动转 JSON
return {"message": "欢迎来到咖啡工坊"}
@app.get是装饰器:把函数root与 URL/的 HTTP GET 方法绑定。- 返回
dict,FastAPI 默认使用application/json返回。
2.3 启动命令
我们约定用 uvicorn 启动:
bash
uv run uvicorn chapters.ch03_first_run.main:app --reload --port 8803
参数说明:
| 参数 | 作用 |
|---|---|
chapters.ch03_first_run.main:app |
模块路径:应用对象 |
--reload |
代码变动自动重启(仅开发环境) |
--port |
监听端口;本章固定为 8803 |
启动成功后浏览器访问 http://127.0.0.1:8803/ 即可看到返回的 JSON。
三、章节代码逐行注释
请打开 chapters/ch03_first_run/main.py,每一行都附有中文注释。重点关注:
- 顶部
from fastapi import FastAPI------ 仅引入了一个符号。 app = FastAPI(title=..., version=...)------ 元信息会显示在/docs。@app.get("/")------ 装饰器把函数挂到路由表里。def root()------ 同步处理函数。
四、自动生成的文档
启动后访问以下地址:
http://127.0.0.1:8803/docs------ Swagger UI,可直接调试接口。http://127.0.0.1:8803/redoc------ ReDoc,只读文档风格。
你会看到刚刚定义的 GET / 接口,且元信息正是我们传入的 title / version。
五、本章小结
| 学到 | 说明 |
|---|---|
| FastAPI 实例 | app = FastAPI(...) 是应用入口 |
| 装饰器 | @app.get 等用于注册路由 |
| 返回值 | dict / BaseModel / str 都自动 JSON |
| 启动命令 | uvicorn 模块:app --port 8803 |
| 自动文档 | /docs 与 /redoc 立即可用 |
下一章我们会把 / 扩展成"咖啡菜单"相关接口,自然延伸到 基础路由与请求方式。
第 04 章 基础路由与请求方式
在第 03 章我们跑起来一个
GET /。本章把它扩展为一个完整的 咖啡订单 REST 接口 ,演示所有常用 HTTP 方式。端口约定:8804。
一、HTTP 方法与业务动作的对应
REST 设计中,每个方法都有明确语义:
| 方法 | 语义 | 咖啡工坊示例 |
|---|---|---|
| GET | 读取资源 | 查看菜单 / 查看订单 |
| POST | 创建资源 | 新建订单 |
| PUT | 全量替换资源 | 修改订单的全部字段 |
| PATCH | 部分更新资源 | 修改订单的状态 |
| DELETE | 删除资源 | 取消订单 |
二、章节代码位置
chapters/ch04_routes/main.py 中我们实现:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /menu |
菜单列表 |
| GET | /menu/{item_id} |
菜单详情 |
| POST | /orders |
下单 |
| GET | /orders |
查询所有订单 |
| GET | /orders/{oid} |
订单详情 |
| PATCH | /orders/{oid} |
修改订单状态 |
| DELETE | /orders/{oid} |
取消订单 |
三、关键代码拆解
3.1 GET 列表
python
@app.get("/menu", tags=["菜单"])
def list_menu() -> list[dict]:
return MENU
3.2 GET 详情 + 路径参数
python
@app.get("/menu/{item_id}")
def get_menu_item(item_id: int) -> dict:
for item in MENU:
if item["id"] == item_id:
return item
raise HTTPException(status_code=404, detail="找不到该咖啡")
{item_id}会被自动捕获,并通过类型提示int自动转换为整数。转换失败(如
/menu/abc)会得到 422 错误。
3.3 POST 创建订单
python
@app.post("/orders", status_code=201)
def create_order(payload: dict) -> dict:
...
status_code=201让成功创建的资源返回201 Created,符合 REST 习惯。
3.4 PUT / PATCH / DELETE
- PUT:客户端必须传完整字段,服务端全量覆盖。
- PATCH:只更新客户端发送的字段。
- DELETE:删除资源并返回 204。
四、运行验证
bash
uv run uvicorn chapters.ch04_routes.main:app --reload --port 8804
依次执行:
bash
curl -X POST http://127.0.0.1:8804/orders \
-H "Content-Type: application/json" \
-d "{\"customer\":\"小明\",\"item_id\":1,\"cups\":2}"
curl http://127.0.0.1:8804/orders
curl -X DELETE http://127.0.0.1:8804/orders/1
观察不同 HTTP 方法返回的状态码与响应体。
五、本章要点
| 学到 | 说明 |
|---|---|
| 装饰器 | @app.get/post/put/patch/delete |
| 路径参数 | {item_id} + 类型注解自动转换 |
| 状态码 | 通过 status_code 自定义 |
| 错误响应 | HTTPException 即抛即返 |
| 路径 / 方法 | REST 语义清晰 |
下一章我们会深入 路径参数、查询参数、请求体 三大参数的细节,并补齐类型提示。
第 05 章 三大请求参数详解
第 04 章我们用
dict接住了请求体。本章升级为 类型化参数 ,并彻底讲清楚 Path / Query / Body 三大参数的来源、用途与细节。
端口约定:8805。
一、参数地图
text
HTTP 请求
│
┌────────┼─────────┐
│ │
│ │
路径 (Path) 查询 (Query) 请求体 (Body)
/orders/{oid} ?status=paid JSON / 表单
出现在 URL 路径里 出现在 ? 之后 出现在 Body 中
| 参数类型 | 在 FastAPI 中表示 | 典型场景 |
|---|---|---|
| Path | oid: int |
资源标识 |
| Query | q: str |
过滤、分页、搜索 |
| Body | payload: T |
创建 / 更新复杂结构 |
二、本章业务:咖啡订单查询
text
GET /orders?status=pending&page=1&size=10
status、page、size都是 Query 参数。
新增 POST /orders 时传入 JSON Body:
json
{"customer":"小明","item_id":1,"cups":2,"note":"少糖"}
这是 Body 参数,用 Pydantic 模型声明。
三、Path 参数高级写法
python
from typing import Annotated
from fastapi import Path
@app.get("/orders/{oid}")
def get_order(oid: Annotated[int, Path(ge=1, description="订单ID,必须 ≥ 1")]):
...
Annotated[T, Path(...)]是 3.9+ 推荐写法,把元信息与类型分开。ge=1表示大于等于 1,越界自动返回 422。
四、Query 参数细节
python
from fastapi import Query
@app.get("/orders")
def list_orders(
status: Annotated[str | None, Query(description="过滤状态")] = None,
page: Annotated[int, Query(ge=1, le=999)] = 1,
size: Annotated[int, Query(ge=1, le=100)] = 20,
):
...
要点:
- 默认值让参数变可选。
Annotated[..., Query(...)]同时提供类型与校验。
五、Body 参数与 Pydantic
python
from pydantic import BaseModel, Field
class OrderIn(BaseModel):
customer: str = Field(min_length=1, max_length=50)
item_id: int = Field(ge=1)
cups: int = Field(ge=1, le=20)
note: str | None = None
OrderIn 会在请求到达时自动校验,并直接给出强类型对象。
六、本章代码运行
bash
uv run uvicorn chapters.ch05_params.main:app --reload --port 8805
测试用例:
bash
# 路径参数 + 查询参数
curl "http://127.0.0.1:8805/orders/1?note=hello"
# 查询参数 + 分页
curl "http://127.0.0.1:8805/orders?status=pending&page=1&size=5"
# Body 参数
curl -X POST http://127.0.0.1:8805/orders \
-H "Content-Type: application/json" \
-d "{\"customer\":\"小明\",\"item_id\":1,\"cups\":2}"
七、章节要点
| 学到 | 说明 |
|---|---|
| Path | URL 路径中的变量 |
| Query | URL ?k=v 部分 |
| Body | JSON / 表单数据 |
| Annotated | Python 3.9+ 推荐的"类型 + 元信息"组合方式 |
| Pydantic | Body 的最佳搭档,自动校验 |
下一章我们会在 Body 上进一步做字段级精细校验(正则、枚举、嵌套模型)。
第 06 章 参数校验
第 05 章我们已经让 Pydantic 做了基础校验。本章把校验做到 字段级 :
正则、枚举、嵌套、列表元素、Email、手机号、URL 格式......全部交给 Pydantic。
端口约定:8806。
一、为什么需要细粒度校验
| 场景 | 例子 | 没有校验的后果 |
|---|---|---|
| 邮箱 | someone@x |
注册后无法找回密码 |
| 手机号 | 12345 |
无法下发短信 |
| 价格 | -10 |
订单金额错误 |
| 枚举值 | status=foo |
数据库写入脏数据 |
| 列表元素 | 购物车传入负数量 | 业务逻辑抛异常 |
二、Field 常用约束
python
from pydantic import Field
from decimal import Decimal
price: Decimal = Field(ge=0, le=999, decimal_places=2)
ge/le/gt/lt:数值边界。min_length/max_length:字符串 / 容器长度。pattern:正则校验字符串。description:自动写入 OpenAPI。
三、字符串格式校验
Pydantic v2 内置了 EmailStr / HttpUrl / IPvAnyAddress 等。
python
from pydantic import EmailStr, HttpUrl
customer_email: EmailStr
shop_website: HttpUrl
这些类型在校验失败时会返回非常清晰的 422 错误。
四、枚举与字面量
python
from enum import Enum
class Roast(str, Enum):
LIGHT = "浅烘"
MEDIUM = "中烘"
DARK = "深烘"
roast: Roast
或者使用 Python 3.12+ 的 Literal:
python
from typing import Literal
roast: Literal["浅烘", "中烘", "深烘"]
五、嵌套模型
python
class OrderItem(BaseModel):
item_id: int
cups: int = Field(ge=1)
class OrderIn(BaseModel):
customer: str
items: list[OrderItem] = Field(min_length=1)
items 数组中每个元素都会按 OrderItem 校验。
六、自定义校验器
python
from pydantic import field_validator
class CustomerIn(BaseModel):
name: str
@field_validator("name")
@classmethod
def name_must_not_be_blank(cls, v: str) -> str:
if not v.strip():
raise ValueError("姓名不能为空")
return v.strip()
七、本章代码要点
chapters/ch06_validation/main.py 中我们落地了:
- 咖啡豆库存模型
BeanIn:price>=0、产地枚举、烘焙度枚举。 - 顾客模型
CustomerIn:邮箱、手机号正则、姓名清洗。 - 订单模型
OrderIn:嵌套OrderItem,最少 1 件、最多 10 件。
八、运行验证
bash
uv run uvicorn chapters.ch06_validation.main:app --reload --port 8806
依次尝试发送非法数据,观察 422 响应:
bash
curl -X POST http://127.0.0.1:8806/customers \
-H "Content-Type: application/json" \
-d "{\"name\":\" \",\"email\":\"not-an-email\"}"
九、本章要点
| 学到 | 说明 |
|---|---|
| Field 约束 | 数值 / 长度 / 正则 |
| 内置格式 | EmailStr / HttpUrl / IPvAnyAddress |
| 枚举 | Enum 或 Literal |
| 嵌套 | 列表元素也是 Pydantic 模型 |
| 自定义校验 | @field_validator |
下一章我们把校验好的数据包装成响应:status_code、headers、response_model、Cookie。
第 07 章 响应数据处理
之前我们直接
return dict。本章介绍 FastAPI 的 响应模型 、自定义状态码 、响应头 / Cookie 、多种 Response 类型 ,并把所有要点串到一份咖啡订单接口里。
端口约定:8807。
一、为什么要控制响应
| 需求 | 做法 |
|---|---|
| 屏蔽敏感字段 | response_model=BeanPublic |
| 返回特定状态码 | status_code=201 |
| 告诉客户端缓存多久 | Response.headers["Cache-Control"] |
| 设置 Cookie | response.set_cookie(...) |
| 重定向 | RedirectResponse(...) |
| 返回纯文本 | PlainTextResponse(...) |
| 文件下载 | FileResponse(...) |
二、response_model
python
class BeanIn(BaseModel):
name: str
price: float
class BeanOut(BaseModel):
id: int
name: str
price: float
@app.post("/beans", response_model=BeanOut, status_code=201)
def add_bean(payload: BeanIn): ...
即便函数里返回了多余字段,response_model 也会只保留模型中的字段 ,
起到"白名单"作用。
进阶:过滤选项
python
response_model=BeanOut,
response_model_exclude_none=True, # 字段为 None 时不返回
response_model_exclude_unset=True, # 客户端没传就不返回
三、自定义状态码
python
from fastapi import status
@app.post("/orders", status_code=status.HTTP_201_CREATED)
def create_order(...): ...
status 模块集中了所有 HTTP 状态码常量,便于语义化。
四、Response 家族
| Response 类型 | 适用 |
|---|---|
JSONResponse |
默认 JSON |
PlainTextResponse |
纯文本 |
HTMLResponse |
HTML 片段 |
RedirectResponse |
302/307 重定向 |
FileResponse |
文件下载 |
StreamingResponse |
流式响应(生成器) |
Response |
基类,可任意控制 header |
python
from fastapi.responses import PlainTextResponse, RedirectResponse
@app.get("/health", response_class=PlainTextResponse)
def health():
return "ok"
@app.get("/old-path")
def old():
return RedirectResponse(url="/new-path", status_code=307)
五、设置 Headers / Cookies
python
from fastapi import Response
@app.get("/set-cookie")
def set_cookie(response: Response):
response.set_cookie(key="user", value="alice", max_age=3600)
response.headers["X-Custom"] = "demo"
return {"ok": True}
六、章节代码要点
chapters/ch07_response/main.py 演示:
BeanOut+response_model屏蔽内部字段。POST /beans返回 201。GET /health返回纯文本。GET /redirect触发 302。GET /set设置 Cookie 与自定义 Header。
七、运行与验证
bash
uv run uvicorn chapters.ch07_response.main:app --reload --port 8807
bash
curl -i http://127.0.0.1:8807/health
curl -i http://127.0.0.1:8807/redirect
curl -i http://127.0.0.1:8807/set
观察响应头里的 content-type、location、set-cookie。
八、本章要点
| 学到 | 说明 |
|---|---|
| response_model | 自动过滤 / 校验响应体 |
| status_code | 默认 200,可自定义 |
| response_class | 整路由统一使用某种 Response |
| Response 对象 | 注入到处理函数后控制 headers / cookies |
| Response 家族 | JSON / Text / HTML / File / Redirect / Stream |
下一章进入"看得见"的部分:静态文件 + 模板渲染。
第 08 章 静态文件与模板渲染
之前所有响应都是 JSON。本章给咖啡工坊加上"门面":HTML 首页 和 CSS / 图片 。
端口约定:8808。
一、为什么需要模板
| 数据格式 | 适合 |
|---|---|
| JSON | 给前端 / 移动端调用 |
| HTML | 浏览器直接访问 |
| 文件下载 | PDF / Excel / 压缩包 |
FastAPI 内置 StaticFiles 处理静态资源,Jinja2Templates 处理 HTML 模板。
二、目录约定
text
chapters/ch08_static_templates/
├── main.py
├── static/
│ ├── css/
│ │ └── style.css
│ └── img/
│ └── logo.svg
└── templates/
├── base.html
├── index.html
└── menu.html
三、挂载静态文件
python
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="static"), name="static")
之后 http://127.0.0.1:8808/static/css/style.css 即可访问。
四、Jinja2 模板
python
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
@app.get("/", response_class=HTMLResponse)
def home(request: Request):
return templates.TemplateResponse(
"index.html",
{"request": request, "shop": "咖啡工坊"},
)
注意:
- 上下文中必须包含
request(Starlette 模板约定)。 templates.TemplateResponse返回TemplateResponse对象,可作为 Response。
五、模板语法
templates/menu.html:
html
{% extends "base.html" %}
{% block content %}
<h1>{{ shop }} · 菜单</h1>
<ul>
{% for item in menu %}
<li>{{ item.name }} --- ¥{{ item.price }}</li>
{% endfor %}
</ul>
{% endblock %}
六、章节代码要点
chapters/ch08_static_templates/main.py:
- 挂载
/static。 - 渲染
index.html:欢迎语 + 当前时间。 - 渲染
menu.html:遍历内存菜单。 - 返回
welcome.html:表单提交(GET / POST)。
七、运行验证
bash
uv run uvicorn chapters.ch08_static_templates.main:app --reload --port 8808
浏览器访问:
http://127.0.0.1:8808/--- 首页http://127.0.0.1:8808/menu--- 菜单页http://127.0.0.1:8808/static/css/style.css--- 静态资源
八、本章要点
| 学到 | 说明 |
|---|---|
| StaticFiles | 一行挂载静态目录 |
| Jinja2Templates | 模板继承、变量、控制流 |
| TemplateResponse | 必须传入 request |
| 目录约定 | static/ + templates/ 是常见组合 |
下一章进入"看得见的数据交换":文件上传与下载。
第 09 章 文件上传与下载
上一章用 Jinja2 渲染页面,本章让用户把菜谱 PDF / 咖啡豆图片上传到服务器 ,
也可以把成品资料下载下来。端口约定:8809。
一、两类操作
| 操作 | 方向 | FastAPI 工具 |
|---|---|---|
| 上传 | 客户端 → 服务端 | UploadFile = File(...) |
| 下载 | 服务端 → 客户端 | FileResponse / StreamingResponse |
二、上传表单(multipart/form-data)
python
from fastapi import UploadFile, File
@app.post("/upload")
async def upload_image(file: UploadFile = File(...)):
content = await file.read()
...
UploadFile包装了 SpooledTemporaryFile,大文件不会全部读入内存。await file.read()/await file.write(...)是异步方法。file.filename/file.content_type给出元信息。
三、保存到磁盘
python
import pathlib
UPLOAD_DIR = pathlib.Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
async def save(file: UploadFile) -> str:
dest = UPLOAD_DIR / file.filename
data = await file.read()
dest.write_bytes(data)
return str(dest)
四、限制上传大小 / 类型
python
@app.post("/upload")
async def upload(file: UploadFile = File(..., max_length=2*1024*1024)): # 2MB
if file.content_type not in {"image/png", "image/jpeg"}:
raise HTTPException(400, "仅支持 PNG / JPEG")
...
max_length 在新版 Starlette 中可能名称不同,常见做法是在中间件或自己读取后判断。
五、下载:FileResponse
python
from fastapi.responses import FileResponse
@app.get("/download/{name}")
def download(name: str):
path = UPLOAD_DIR / name
if not path.exists():
raise HTTPException(404, "文件不存在")
return FileResponse(path, filename=name, media_type="application/octet-stream")
六、下载:StreamingResponse
python
from fastapi.responses import StreamingResponse
@app.get("/stream")
def stream():
def gen():
for i in range(5):
yield f"chunk {i}\n".encode()
return StreamingResponse(gen(), media_type="text/plain")
适合 大文件 或 实时数据流。
七、章节代码要点
chapters/ch09_files/main.py:
POST /upload单文件上传(菜谱 PDF)。POST /upload-multiple多文件上传(多张豆图)。GET /list列出已上传文件。GET /download/{name}下载文件。GET /stream流式输出示例。
八、运行验证
bash
uv run uvicorn chapters.ch09_files.main:app --reload --port 8809
bash
# 单文件上传
curl -F "file=@./recipe.pdf" http://127.0.0.1:8809/upload
# 多文件上传
curl -F "files=@./a.jpg" -F "files=@./b.jpg" http://127.0.0.1:8809/upload-multiple
九、本章要点
| 学到 | 说明 |
|---|---|
| UploadFile | 异步、大文件友好 |
| File | 与 UploadFile 配合的多部件表单字段 |
| FileResponse | 一次性读取并发送文件 |
| StreamingResponse | 生成器输出,适合流式场景 |
下一章进入"出错怎么办":异常处理与全局异常捕获。
第 10 章 异常处理与全局异常捕获
业务逻辑出错时直接
raise HTTPException是基础做法。本章我们构建一份业务异常类 + 全局异常处理器 ,让错误响应统一、可扩展。
端口约定:8810。
一、FastAPI 默认行为
python
raise HTTPException(status_code=404, detail="订单不存在")
响应:
json
{"detail": "订单不存在"}
简单场景够用,但当业务复杂时(不同异常 → 不同 JSON 结构、不同状态码)就要:
- 自定义异常类;
- 注册
@app.exception_handler; - 覆盖默认
HTTPException/RequestValidationError。
二、自定义业务异常
python
class BusinessError(Exception):
def __init__(self, code: str, message: str, status_code: int = 400):
self.code = code
self.message = message
self.status_code = status_code
业务代码:
python
order = ORDERS.get(oid)
if order is None:
raise BusinessError("ORDER_NOT_FOUND", f"订单 {oid} 不存在", 404)
三、全局异常处理器
python
from fastapi.responses import JSONResponse
@app.exception_handler(BusinessError)
async def handle_business(request: Request, exc: BusinessError):
return JSONResponse(
status_code=exc.status_code,
content={"code": exc.code, "message": exc.message},
)
四、统一请求校验错误格式
python
from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def handle_validation(request, exc):
return JSONResponse(
status_code=422,
content={
"code": "VALIDATION_ERROR",
"errors": exc.errors(),
},
)
五、捕获未处理异常(兜底)
python
@app.exception_handler(Exception)
async def handle_all(request, exc):
return JSONResponse(
status_code=500,
content={"code": "INTERNAL", "message": "服务器开了个小差,请稍后再试"},
)
不要在生产环境把
str(exc)直接暴露给客户端,可能泄露内部细节。
六、章节代码要点
chapters/ch10_exception/main.py:
- 定义
BusinessError+ 错误码常量。 - 注册三种处理器:业务错误 / 参数校验 / 兜底。
- 演示:找不到订单、库存不足、字段错误、服务异常。
七、运行验证
bash
uv run uvicorn chapters.ch10_exception.main:app --reload --port 8810
bash
# 触发业务错误
curl -i http://127.0.0.1:8810/orders/9999
# 触发参数校验错误
curl -X POST http://127.0.0.1:8810/orders \
-H "Content-Type: application/json" \
-d '{"customer":"","cups":0}'
# 触发兜底
curl -i http://127.0.0.1:8810/boom
八、本章要点
| 学到 | 说明 |
|---|---|
| HTTPException | 内置快捷异常 |
| 自定义异常 | 业务可控的 BusinessError |
| exception_handler | 装饰器注册全局处理 |
| RequestValidationError | 422 校验错误的统一接管 |
| 兜底 Exception | 防止 traceback 暴露到生产环境 |
下一章进入"中间件层":中间件与跨域。
第 11 章 中间件与跨域
中间件是 FastAPI 处理请求的"流水线"。本章加入 请求耗时日志 和 CORS 跨域 。
端口约定:8811。
一、中间件是什么
text
请求 ──▶ M1 ──▶ M2 ──▶ 路由处理函数 ──▶ M2 ──▶ M1 ──▶ 响应
每个中间件可以:
- 在调用下游前修改
request。 - 在下游返回后修改
response。 - 短路(直接返回,不调用下游)。
二、两种定义方式
2.1 装饰器(推荐)
python
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
cost = (time.perf_counter() - start) * 1000
response.headers["X-Process-Time-ms"] = f"{cost:.2f}"
return response
2.2 类形式
python
from starlette.middleware.base import BaseHTTPMiddleware
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
start = time.perf_counter()
response = await call_next(request)
response.headers["X-Process-Time-ms"] = f"{(time.perf_counter()-start)*1000:.2f}"
return response
app.add_middleware(TimingMiddleware)
三、CORS(跨域资源共享)
浏览器同源策略会阻止 a.com 调 b.com 的 API。CORS 通过响应头告诉浏览器"我允许谁"。
python
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://shop.example.com"], # 或 ["*"] 表示全部
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
四、自定义中间件举例
4.1 请求 ID
python
import uuid
@app.middleware("http")
async def add_request_id(request: Request, call_next):
rid = request.headers.get("X-Request-ID", str(uuid.uuid4()))
request.state.request_id = rid
response = await call_next(request)
response.headers["X-Request-ID"] = rid
return response
4.2 简单访问日志
python
@app.middleware("http")
async def access_log(request: Request, call_next):
print(f"[{time.strftime('%H:%M:%S')}] {request.method} {request.url.path}")
return await call_next(request)
五、中间件顺序
add_middleware 按调用顺序包裹:
text
请求 → M1(最先 add) → M2 → 路由 → M2 → M1 → 响应
六、章节代码要点
chapters/ch11_middleware/main.py:
- 计时中间件(写入响应头)。
- 请求 ID 中间件。
- 简单访问日志中间件。
- CORS 中间件。
- 健康检查 + 一个示例业务接口。
七、运行验证
bash
uv run uvicorn chapters.ch11_middleware.main:app --reload --port 8811
bash
curl -i http://127.0.0.1:8811/menu # 看响应头 X-Process-Time-ms
curl -i -H "Origin: https://x.com" -H "Access-Control-Request-Method: GET" \
-X OPTIONS http://127.0.0.1:8811/menu
八、本章要点
| 学到 | 说明 |
|---|---|
| @app.middleware("http") | 装饰器定义中间件 |
| BaseHTTPMiddleware | 类形式定义 |
| CORSMiddleware | 浏览器跨域 |
| request.state | 在中间件与处理函数之间共享数据 |
| 中间件顺序 | 先注册的最外层 |
下一章进入"依赖注入":模块化复用。
第 12 章 依赖注入
上一章我们写了一份"内存订单服务"。本章把"获取服务实例"做成 可复用依赖 ,
并展示嵌套依赖、缓存、按参数 yield 资源 等高级用法。端口约定:8812。
一、为什么用 Depends
如果每个接口都写 svc = OrderService(); svc.connect(),会有大量重复。
依赖注入把这些"准备资源"的代码集中起来:
python
def get_order_service() -> OrderService:
return OrderService()
@app.get("/orders/{oid}")
def get_order(oid: int, svc: Annotated[OrderService, Depends(get_order_service)]):
return svc.get(oid)
二、Annotated 写法(推荐)
python
from typing import Annotated
from fastapi import Depends
svc: Annotated[OrderService, Depends(get_order_service)]
Python 3.9+ 风格,把类型与"依赖来源"分离。
三、类作为依赖
python
class Pagination:
def __init__(
self,
page: Annotated[int, Query(ge=1)] = 1,
size: Annotated[int, Query(ge=1, le=100)] = 20,
):
self.page = page
self.size = size
self.offset = (page - 1) * size
@app.get("/items")
def list_items(p: Annotated[Pagination, Depends()]):
return {"page": p.page, "size": p.size, "offset": p.offset}
四、嵌套依赖
python
def get_db():
return DatabaseConn()
def get_user(db: Annotated[DatabaseConn, Depends(get_db)]):
return db.get_current_user()
@app.get("/profile")
def profile(user: Annotated[User, Depends(get_user)]):
return user.dict()
五、yield 依赖(资源自动关闭)
python
def get_db_session():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/items")
def create(db: Annotated[Session, Depends(get_db_session)], payload: ItemIn):
db.add(Item(**payload.model_dump()))
db.commit()
yield 之前 = "前置",之后 = "后置清理"。FastAPI 会保证 finally 一定执行。
六、章节代码要点
chapters/ch12_dependency/main.py:
get_settings()提供配置。get_clock()提供当前时间(演示依赖嵌套)。Pagination类作为依赖。get_order_service()yield 模拟数据库连接。- 三个业务路由使用上述依赖。
七、运行验证
bash
uv run uvicorn chapters.ch12_dependency.main:app --reload --port 8812
bash
curl "http://127.0.0.1:8812/orders?page=2&size=5"
curl "http://127.0.0.1:8812/info"
curl "http://127.0.0.1:8812/now"
八、本章要点
| 学到 | 说明 |
|---|---|
| Depends | 注入一个可调用对象作为依赖 |
| Annotated | 类型 + 依赖的推荐写法 |
| 类依赖 | 适合"参数包"如 Pagination |
| yield 依赖 | 自动管理资源生命周期 |
| 缓存 | 同一请求内依赖只执行一次 |
下一章进入"访问控制":基础认证方式。
第 13 章 基础认证方式
咖啡工坊后台只能让员工访问。本章介绍三种轻量级认证:
HTTPBasic / APIKey / 简单 Bearer ,不依赖 OAuth/JWT。
端口约定:8813。
一、HTTP Basic
客户端在 Header 发送 Authorization: Basic base64(user:pass)。
服务端解码后校验用户名密码。
python
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import secrets
security = HTTPBasic()
def check_cred(creds: Annotated[HTTPBasicCredentials, Depends(security)]):
ok_user = secrets.compare_digest(creds.username, "alice")
ok_pass = secrets.compare_digest(creds.password, "secret")
if not (ok_user and ok_pass):
raise HTTPException(401, "认证失败", headers={"WWW-Authenticate": "Basic"})
return creds.username
secrets.compare_digest 防时序攻击。
二、API Key
通过 Header / Query 传一个固定 key。
python
from fastapi.security import APIKeyHeader
api_key = APIKeyHeader(name="X-API-Key")
def check_key(key: Annotated[str, Depends(api_key)]):
if key != "my-secret-key":
raise HTTPException(403, "Invalid API Key")
return key
客户端:
bash
curl -H "X-API-Key: my-secret-key" http://...
三、简易 Bearer
python
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
bearer = HTTPBearer()
def check_bearer(token: Annotated[HTTPAuthorizationCredentials, Depends(bearer)]):
if token.credentials != "my-token":
raise HTTPException(403, "Invalid token")
return token.credentials
四、401 与 403
- 401 Unauthorized:未提供凭证 / 凭证错误。
- 403 Forbidden:凭证有效但无权访问。
HTTPException(401, ..., headers={"WWW-Authenticate": "Basic"}) 是标准做法。
五、章节代码要点
chapters/ch13_auth_basic/main.py:
- 员工清单(用户名 + bcrypt 哈希)。
- HTTPBasic 保护
/admin/orders。 - APIKey 保护
/internal/stats。 - Bearer 保护
/api/secret。 - 公开接口:
/login(明文登录演示)+/healthz。
六、运行验证
bash
uv run uvicorn chapters.ch13_auth_basic.main:app --reload --port 8813
bash
# Basic
curl -u alice:secret http://127.0.0.1:8813/admin/orders
# API Key
curl -H "X-API-Key: my-secret-key" http://127.0.0.1:8813/internal/stats
# Bearer
curl -H "Authorization: Bearer my-token" http://127.0.0.1:8813/api/secret
七、本章要点
| 学到 | 说明 |
|---|---|
| HTTPBasic | 内置基础认证 |
| APIKeyHeader | 自定义 Header / Query 传 Key |
| HTTPBearer | 简易 Token |
| secrets.compare_digest | 防时序攻击的字符串比较 |
| 401 vs 403 | 认证失败 vs 无权限 |
下一章进入真正的"令牌认证":JWT。
第 14 章 JWT 令牌认证
上一章我们用静态 Token 鉴权。本章换成真正的 JWT(JSON Web Token) :
登录后签发令牌,后续接口通过令牌识别用户身份。端口约定:8814。
一、JWT 三段式
text
xxxxx.yyyyy.zzzzz
↑ ↑ ↑
header payload signature
- header:算法 / 类型。
- payload:业务数据(用户 ID、过期时间等)。
- signature:用密钥对前两段签名,防篡改。
二、关键依赖
python
from jose import jwt, JWTError
python-jose 是 Python 生态里最常用的 JWT 库。
三、签发令牌
python
SECRET = "change-me"
ALG = "HS256"
def create_token(sub: str, ttl_seconds: int = 3600) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": sub,
"iat": now,
"exp": now + timedelta(seconds=ttl_seconds),
}
return jwt.encode(payload, SECRET, algorithm=ALG)
四、校验令牌
python
def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> str:
try:
payload = jwt.decode(token, SECRET, algorithms=[ALG])
except JWTError:
raise HTTPException(401, "Invalid token")
return payload["sub"]
五、登录流程
text
客户端 服务端
│ │
│── POST /login {user, pass} ────────▶ │
│ │ 校验 bcrypt
│◀── {access_token: "xxxx.yyyy.zzzz"} ─│
│ │
│── GET /me Authorization: Bearer ... ─▶│
│ │ 解析 JWT → sub
│◀── {user: "alice"} ──────────────── │
六、FastAPI 内置 OAuth2 工具
python
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
tokenUrl="login" 会让 Swagger UI 显示"Authorize"按钮,调试更方便。
七、章节代码要点
chapters/ch14_auth_jwt/main.py:
- 内存员工表 + bcrypt 哈希密码。
POST /login:校验密码 → 签发 JWT。GET /me:解析 Bearer Token,返回当前用户。POST /admin/orders:员工后台,依赖get_current_user。- 公开接口
/healthz。
八、运行验证
bash
uv run uvicorn chapters.ch14_auth_jwt.main:app --reload --port 8814
bash
# 登录拿 token
TOKEN=$(curl -s -X POST http://127.0.0.1:8814/login \
-d "username=alice&password=secret" | jq -r .access_token)
# 访问受保护接口
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8814/me
九、本章要点
| 学到 | 说明 |
|---|---|
| JWT 结构 | header.payload.signature |
| jwt.encode / decode | 签发 / 校验 |
| OAuth2PasswordBearer | FastAPI 内置 OAuth2 工具 |
| 过期时间 | exp 字段自动校验 |
| 401 vs 403 | 凭证缺失 / 凭证错误 |
下一章开始"工程化":规范项目目录结构。
第 15 章 规范项目目录结构
前面 14 章每个示例都在单个
main.py里。真实项目必然要拆分:路由 / 模型 / 服务 / 配置 / 依赖 各司其职。本章用 APIRouter + 模块化包 重构订单系统。
端口约定:8815。
一、目标结构
text
chapters/ch15_structure/
├── main.py # 仅负责启动 + 装载路由
├── app/
│ ├── __init__.py
│ ├── config.py # Settings
│ ├── deps.py # 公共依赖
│ ├── schemas.py # Pydantic 模型
│ ├── services.py # 业务逻辑
│ └── routers/
│ ├── __init__.py
│ ├── orders.py # /orders 子路由
│ ├── menu.py # /menu 子路由
│ └── health.py # /healthz 子路由
二、拆分原则
| 模块 | 职责 |
|---|---|
config |
读取环境变量、配置常量 |
schemas |
Pydantic 输入 / 输出模型 |
services |
与数据库 / 第三方交互的业务逻辑 |
routers |
FastAPI 路由(薄薄一层,调用 service) |
deps |
复用的依赖(鉴权、上下文、分页等) |
main |
FastAPI() 实例 + include_router |
三、关键 API:APIRouter
python
# app/routers/orders.py
from fastapi import APIRouter
router = APIRouter(prefix="/orders", tags=["订单"])
@router.get("/")
def list_orders():
...
@router.get("/{oid}")
def get_order(oid: int):
...
主应用:
python
# main.py
from app.routers import orders, menu, health
app.include_router(orders.router)
app.include_router(menu.router)
app.include_router(health.router)
四、配置:pydantic-settings(推荐)
python
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
shop_name: str = "咖啡工坊"
tax_rate: float = 0.06
debug: bool = False
class Config:
env_prefix = "COFFEE_" # 读 COFFEE_SHOP_NAME 等
settings = Settings()
pydantic-settings不在基础依赖里,本章先用简单 dataclass 实现。
五、章节代码要点
chapters/ch15_structure/:
app/config.py:Settings。app/schemas.py:OrderIn / OrderOut。app/services.py:OrderService。app/deps.py:分页依赖。app/routers/orders.py:订单路由。app/routers/menu.py:菜单路由。app/routers/health.py:健康检查。main.py:装配。
六、运行验证
bash
uv run uvicorn chapters.ch15_structure.main:app --reload --port 8815
bash
curl http://127.0.0.1:8815/orders
curl http://127.0.0.1:8815/menu
curl http://127.0.0.1:8815/healthz
curl http://127.0.0.1:8815/docs # 各 router 的 tags 已分组
七、本章要点
| 学到 | 说明 |
|---|---|
| APIRouter | 路由拆分 |
| include_router | 把子路由挂到主应用 |
| 模块边界 | config / schemas / services / routers / deps |
| tags | 在 Swagger UI 中按业务分组 |
下一章进入"持久化":数据库的联动开发。
第 16 章 数据库的联动开发
之前所有数据都在内存里。本章引入 SQLAlchemy 2.0 + SQLite ,把订单持久化。
端口约定:8816。
一、SQLAlchemy 2.0 新写法
老写法:
python
db.execute("SELECT * FROM orders")
新写法(推荐):
python
stmt = select(Order).where(Order.id == 1)
result = db.execute(stmt)
二、组件分层
text
config - 数据库 URL
db.py - engine / SessionLocal / Base
models.py - ORM 模型
schemas.py - Pydantic 模型
crud.py - 增删改查函数
routers/ - FastAPI 路由
main.py - 应用入口
三、Engine + Session
python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
DATABASE_URL = "sqlite:///./coffeecraft.db"
engine = create_engine(DATABASE_URL, echo=False, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(bind=engine, autoflush=False)
class Base(DeclarativeBase):
pass
四、ORM 模型
python
from sqlalchemy import String, Integer, Float
from sqlalchemy.orm import Mapped, mapped_column
class CoffeeBean(Base):
__tablename__ = "beans"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(60))
price: Mapped[float] = mapped_column(Float)
五、依赖:每请求一个 Session
python
from typing import Generator
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()
路由里使用:
python
@app.post("/beans")
def add(payload: BeanIn, db: Annotated[Session, Depends(get_db)]):
bean = CoffeeBean(**payload.model_dump())
db.add(bean)
db.commit()
db.refresh(bean)
return bean
六、章节代码要点
chapters/ch16_database/:
db.py引擎 + Session + Base。models.pyCoffeeBean / Order / OrderItem。schemas.py输入输出模型。crud.py业务函数。routers/路由。main.py应用入口。
七、运行验证
bash
uv run uvicorn chapters.ch16_database.main:app --reload --port 8816
bash
curl -X POST http://127.0.0.1:8816/beans \
-H "Content-Type: application/json" \
-d '{"name":"瑰夏","price":380}'
curl http://127.0.0.1:8816/beans
数据库文件 coffeecraft.db 会自动生成。
八、本章要点
| 学到 | 说明 |
|---|---|
| DeclarativeBase | SQLAlchemy 2.0 声明基类 |
| Mapped / mapped_column | 类型化字段声明 |
| get_db 依赖 | 每请求自动获取 + 关闭 Session |
| Session.add / commit / refresh | 增删改的标准流程 |
| SQLite | 零配置数据库,演示用 |
更新提示 :本章已改用
lifespan异步上下文替代已废弃的@app.on_event("startup"),建表语句放在
lifespan中执行,不在 import 时副作用。生产请把
Base.metadata.create_all替换为 Alembic 迁移。
下一章进入"演进":接口版本管理。
第 17 章 接口版本管理
业务会演进:旧字段要弃用、新字段要添加。最稳妥的方案是 URL 版本 。
本章用
/api/v1/与/api/v2/两套路由共存演示平滑升级。端口约定:8817。
一、版本控制三大流派
| 流派 | 示例 | 优缺点 |
|---|---|---|
| URL 路径 | /api/v1/orders |
最清晰,主流首选 |
| Header | Accept: application/vnd.coffee.v2+json |
路径干净,但需要文档支持 |
| 查询参数 | /orders?version=2 |
简单但易被忽略 |
本章使用 URL 路径。
二、目录约定
text
chapters/ch17_versioning/
├── main.py
├── v1/
│ ├── __init__.py
│ ├── router.py # v1 路由
│ └── schemas.py # v1 模型
└── v2/
├── __init__.py
├── router.py # v2 路由
└── schemas.py # v2 模型(字段可能不同)
三、版本差异示例
v1 的 OrderOut:
python
class OrderOut(BaseModel):
id: int
item: str
cups: int
v2 的 OrderOut:
python
class OrderOut(BaseModel):
id: int
items: list[OrderItem] # 结构化、扩展
customer: str
created: datetime
status: str
四、挂载不同版本
python
app.include_router(v1_router, prefix="/api/v1")
app.include_router(v2_router, prefix="/api/v2")
两个版本可以同时运行,老客户端不需立刻升级。
五、章节代码要点
chapters/ch17_versioning/:
- v1:返回简单结构。
- v2:返回丰富结构,新增
customer / created字段。 - 公共:依赖注入共享同一份内存数据。
六、运行验证
bash
uv run uvicorn chapters.ch17_versioning.main:app --reload --port 8817
bash
curl http://127.0.0.1:8817/api/v1/orders
curl http://127.0.0.1:8817/api/v2/orders
观察两个版本返回结构不同。
七、本章要点
| 学到 | 说明 |
|---|---|
| URL 版本 | /api/v1、/api/v2 |
| include_router | 同一进程多版本共存 |
| 字段演进 | 旧版本不删除,只标 deprecated |
| OpenAPI 多版本 | /docs 自动汇总所有版本 |
下一章进入"性能层":同步与异步接口。
第 18 章 同步和异步接口
def与async def的差异决定了一个接口能否真正发挥出 FastAPI 的高并发能力。本章对比两种写法的行为,并给出"何时用哪个"的原则。端口约定:8818。
一、定义
| 写法 | 类型 | 运行模型 |
|---|---|---|
def |
同步函数 | 跑在线程池(anyio worker thread) |
async def |
协程 | 跑在主事件循环 |
二、性能实验
text
def slow_io() → 阻塞线程池 worker
async def slow_io_async() → 让出事件循环,挂起等待
并发请求 100 个时:
- 全用
def:线程池满后排队。 - 全用
async def+await asyncio.sleep:100 个一起处理。 - 混合:FastAPI 自动协调。
三、章节代码要点
chapters/ch18_sync_async/main.py:
GET /sync:def+time.sleep(0.2),阻塞。GET /async:async def+await asyncio.sleep(0.2),非阻塞。GET /io-async:用httpx.AsyncClient调用外部 HTTP(演示真正异步 IO)。GET /healthz:基准。GET /stats:返回当前 worker 使用情况。
四、运行验证
bash
uv run uvicorn chapters.ch18_sync_async.main:app --reload --port 8818
并发测试:
bash
# 启动 5 个并发请求
for i in {1..5}; do
(time curl -s http://127.0.0.1:8818/async) &
done
wait
观察 5 个 /async 请求几乎同时返回,而 5 个 /sync 会按顺序各 200ms。
五、何时用哪个
| 场景 | 选 async def |
选 def |
|---|---|---|
| httpx / aiohttp / asyncpg | ✅ | ❌ |
| requests / 同步 ORM | ❌ | ✅ |
| 重 CPU 计算 | 都可(线程池) |
六、踩坑点
- 在
async def里调用同步阻塞库 → 整个事件循环卡死。 - 想"看似异步"地调用同步库:用
await run_in_threadpool(...)。 - SQLAlchemy 异步版本叫
sqlalchemy.ext.asyncio(第 19 章使用)。
七、本章要点
| 学到 | 说明 |
|---|---|
| def | 跑在线程池 |
| async def | 跑在事件循环 |
| httpx.AsyncClient | 推荐异步 HTTP 客户端 |
| run_in_threadpool | 在 async 中安全调用同步代码 |
下一章进入"异步数据库":异步数据库和异步请求。
第 19 章 异步数据库和异步请求
上一章用 httpx 演示异步 HTTP。本章把 SQLAlchemy 也升级到 异步 :
async_session+aiosqlite。端口约定:8819。
一、SQLAlchemy 异步引擎
python
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
DATABASE_URL = "sqlite+aiosqlite:///./ch19.db"
engine = create_async_engine(DATABASE_URL, echo=False)
SessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)
sqlite+aiosqlite与第 16 章的sqlite://不同:多了驱动名前缀。
二、异步依赖
python
from typing import AsyncGenerator
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with SessionLocal() as session:
yield session
async with 保证关闭。yield 让 FastAPI 在请求结束后回到这里清理。
三、异步 CRUD
python
from sqlalchemy import select
async def list_beans(db: AsyncSession) -> list[CoffeeBean]:
result = await db.execute(select(CoffeeBean))
return list(result.scalars())
async def add_bean(db: AsyncSession, payload: BeanIn) -> CoffeeBean:
bean = CoffeeBean(**payload.model_dump())
db.add(bean)
await db.commit()
await db.refresh(bean)
return bean
注意:所有 IO 操作都要 await。
四、异步 HTTP 请求
python
import httpx
async def fetch_weather():
async with httpx.AsyncClient() as client:
r = await client.get("https://...")
return r.json()
五、章节代码要点
chapters/ch19_async_db/:
db.py异步引擎。models.py复用第 16 章定义。routers/beans.py异步 CRUD。routers/external.py异步 HTTP(httpx)。main.py入口。
六、运行验证
bash
uv run uvicorn chapters.ch19_async_db.main:app --reload --port 8819
bash
# 创建豆子(异步写库)
curl -X POST http://127.0.0.1:8819/beans \
-H "Content-Type: application/json" \
-d '{"name":"耶加雪菲","price":260,"stock":15}'
# 异步 HTTP
curl http://127.0.0.1:8819/external/ip
七、本章要点
| 学到 | 说明 |
|---|---|
| create_async_engine | SQLAlchemy 异步引擎 |
| AsyncSession | 异步 Session |
| async def + await | 一切 IO 都要 await |
| aiosqlite | SQLite 异步驱动 |
| httpx.AsyncClient | 推荐异步 HTTP 客户端 |
更新提示 :本章已改用
lifespan替代已废弃的@app.on_event("startup"),异步建表
await conn.run_sync(Base.metadata.create_all)放在 lifespan 内执行。
下一章进入"压测视角":高并发注意要点。
第 20 章 高并发注意要点
当 QPS 上升到几千上万,"能跑"≠"扛得住"。本章把 压测视角 中常见的坑汇总。
端口约定:8820。本章主要是思路与代码示例。
一、三座大山
- 线程/协程模型:阻塞 vs 非阻塞。
- 数据库连接池:连接数 ≠ QPS。
- 外部依赖限流:httpx 默认连接池、第三方 API 配额。
二、Uvicorn 调优
bash
uvicorn app:app \
--host 0.0.0.0 --port 8820 \
--workers 4 \
--loop uvloop \
--http httptools \
--backlog 2048
| 参数 | 作用 |
|---|---|
--workers |
进程数,建议 = CPU 核数 |
--loop uvloop |
高性能事件循环 |
--http httptools |
高性能 HTTP 解析 |
--backlog |
accept 队列长度 |
三、数据库连接池
python
engine = create_async_engine(
DATABASE_URL,
pool_size=20, # 默认 5
max_overflow=10, # 额外连接
pool_pre_ping=True, # 检测死连接
)
并发 ≈ pool_size + max_overflow;超过则排队。
四、避免阻塞事件循环
python
# ❌ 错误
async def handler():
time.sleep(1) # 阻塞事件循环
# ✅ 正确
async def handler():
await asyncio.sleep(1)
五、限流
python
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.get("/api")
@limiter.limit("5/minute")
def handler(): ...
六、缓存热点
python
from functools import lru_cache
@lru_cache(maxsize=128)
def get_menu(): ... # 内存缓存
或者外置 Redis。
七、章节代码要点
chapters/ch20_concurrency/main.py:
- 启动时输出事件循环 / HTTP 解析器。
- 演示三种 IO 模式(同步 / 异步 / 异步并发)。
- 信号量限制并发。
time.sleep阻塞事件循环的反面教材。
八、运行验证
bash
uv run uvicorn chapters.ch20_concurrency.main:app --reload --port 8820
并发测试(PowerShell):
powershell
1..10 | ForEach-Object -Parallel { Invoke-WebRequest http://127.0.0.1:8820/slow-async } -ThrottleLimit 10
九、本章要点
| 学到 | 说明 |
|---|---|
| 多进程 | workers = CPU 核数 |
| uvloop | 推荐替换 asyncio 默认 loop |
| 连接池 | pool_size + max_overflow 配置 |
| 限流 | slowapi / 自实现 |
| 缓存 | LRU / Redis |
下一章进入"质量保障":日志 / 测试 / 接口文档。
第 21 章 日志 / 测试 / 接口文档
完结前的最后一站"质量三角":可观测(logging) 、可验证(pytest) 、可发现(OpenAPI) 。
端口约定:8821。
一、结构化日志
python
import logging
logger = logging.getLogger("coffeecraft")
logger.setLevel(logging.INFO)
生产推荐用 loguru / structlog,本章用标准库即可。
二、请求日志中间件
python
@app.middleware("http")
async def log_requests(request: Request, call_next):
logger.info("→ %s %s", request.method, request.url.path)
response = await call_next(request)
logger.info("← %s %s %d", request.method, request.url.path, response.status_code)
return response
三、Pytest 测试
python
from fastapi.testclient import TestClient
from chapters.ch21_logging_testing.main import app
def test_health():
client = TestClient(app)
r = client.get("/healthz")
assert r.status_code == 200
TestClient 基于 httpx,同步调用就能测异步接口。
四、pytest-asyncio(异步测试)
python
import pytest
from httpx import AsyncClient, ASGITransport
@pytest.mark.asyncio
async def test_async():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
r = await c.get("/healthz")
assert r.status_code == 200
五、自定义 OpenAPI
python
app = FastAPI(
title="咖啡工坊",
version="1.0.0",
description="...",
openapi_tags=[{"name":"订单","description":"顾客订单相关"}],
contact={"name":"CoffeeCraft","email":"hi@coffee.dev"},
)
还可在 app.openapi 函数里进一步定制(添加 logo、服务器 URL 等)。
六、章节代码要点
chapters/ch21_logging_testing/:
main.py:带 logging 中间件 + 自定义 OpenAPI。test_main.py:用 pytest 测所有接口。
七、运行验证
bash
uv run uvicorn chapters.ch21_logging_testing.main:app --reload --port 8821
# 跑测试
uv run pytest chapters/ch21_logging_testing/ -v
八、本章要点
| 学到 | 说明 |
|---|---|
| logging | 标准库即可;推荐 loguru |
| TestClient | 同步测异步 |
| pytest-asyncio | 真·异步测试 |
| OpenAPI 元信息 | title / description / tags / contact |
下一章进入"上线":项目部署与上线。
第 22 章 项目部署与上线
本教程的最后一站:把咖啡工坊 API 从本机搬到生产服务器 。
我们覆盖最常见的三条路径:Uvicorn + 进程管理、Docker 容器、Nginx 反向代理。
一、必备清单
| 项目 | 命令 / 内容 |
|---|---|
| Python | 3.10+(本项目使用 3.14) |
| 包管理 | uv sync 安装依赖 |
| 启动命令 | uvicorn app:app --host 0.0.0.0 --port 8000 |
| 进程数 | --workers $((2 * $(nproc))) |
| 反向代理 | Nginx / Caddy |
二、生产级 Uvicorn 命令
bash
uvicorn chapters.ch19_async_db.main:app \
--host 0.0.0.0 \
--port 8000 \
--workers 4 \
--loop uvloop \
--http httptools \
--proxy-headers \
--forwarded-allow-ips='*' \
--log-level info
三、Docker 镜像
Dockerfile:
dockerfile
FROM python:3.14-slim
WORKDIR /app
# 安装 uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
# 复制依赖清单并安装
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-cache
# 复制源码
COPY . .
EXPOSE 8000
CMD ["uv", "run", "uvicorn", "chapters.ch19_async_db.main:app", \
"--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
.dockerignore:
.venv
__pycache__
.pytest_cache
*.db
.git
构建与运行:
bash
docker build -t coffeecraft:1.0 .
docker run --rm -p 8000:8000 coffeecraft:1.0
四、Nginx 反向代理
/etc/nginx/conf.d/coffee.conf:
nginx
upstream coffee_app {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name coffee.example.com;
client_max_body_size 10m;
location / {
proxy_pass http://coffee_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
记得在 Uvicorn 启动时加 --proxy-headers 让 request.client.host 取到真实 IP。
五、HTTPS(Caddy 更简单)
Caddyfile:
coffee.example.com {
reverse_proxy 127.0.0.1:8000
}
Caddy 会自动申请 Let's Encrypt 证书。
六、Systemd 进程守护
/etc/systemd/system/coffee.service:
ini
[Unit]
Description=CoffeeCraft API
After=network.target
[Service]
WorkingDirectory=/srv/coffeecraft
ExecStart=/srv/coffeecraft/.venv/bin/uvicorn chapters.ch19_async_db.main:app \
--host 0.0.0.0 --port 8000 --workers 4
Restart=always
User=coffee
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
bash
sudo systemctl enable --now coffee
sudo journalctl -u coffee -f
七、Gunicorn + UvicornWorker(备选)
bash
uv add gunicorn
uv run gunicorn chapters.ch19_async_db.main:app \
-k uvicorn.workers.UvicornWorker \
-w 4 -b 0.0.0.0:8000
八、生产前清单
-
SECRET、JWT_SECRET等敏感值来自环境变量。 - 日志写入文件 + logrotate。
- 数据库有备份 / 主从。
- 接口限流。
- 监控告警(Prometheus / Sentry)。
- 域名 + HTTPS。
- 防火墙只暴露 80/443。
九、本章要点
| 学到 | 说明 |
|---|---|
| Uvicorn | 启动命令 + workers |
| Docker | python:3.14-slim + uv |
| Nginx | 反向代理 / HTTPS |
| Caddy | 自动证书 |
| Systemd | 进程守护 |
| Gunicorn | 经典替代方案 |
十、教程完结 🎉
到这里你已经走完了 22 个章节,覆盖了:
text
第 01-02 章 概念入门
第 03-07 章 基础(路由 / 参数 / 校验 / 响应)
第 08-14 章 进阶(模板 / 文件 / 异常 / 中间件 / DI / 认证)
第 15-22 章 实战(结构 / 数据库 / 版本 / 异步 / 高并发 / 测试 / 部署)
接下来的进阶路径:
- 真实数据库(PostgreSQL)+ Alembic 迁移
- Docker Compose 一键启动依赖
- Kubernetes 部署
- 监控 / 链路追踪 / 灰度发布
- CI/CD 流水线
祝你做出优秀的 API!