咖啡工坊 · FastAPI 22 章教程合集

☕ 咖啡工坊 · 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

  1. 写起来像写注释:类型提示就是文档,编辑器能自动补全。
  2. 错误信息友好:校验失败时返回的 JSON 错误结构清晰,前端能直接渲染。
  3. 生态完整:OAuth2、JWT、SQL 建模、后台任务、WebSocket 一应俱全。
  4. 生产可用:从单文件 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 必须是 stritemslist[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,每一行都附有中文注释。重点关注:

  1. 顶部 from fastapi import FastAPI ------ 仅引入了一个符号。
  2. app = FastAPI(title=..., version=...) ------ 元信息会显示在 /docs
  3. @app.get("/") ------ 装饰器把函数挂到路由表里。
  4. 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
  • statuspagesize 都是 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 中我们落地了:

  1. 咖啡豆库存模型 BeanInprice>=0、产地枚举、烘焙度枚举。
  2. 顾客模型 CustomerIn:邮箱、手机号正则、姓名清洗。
  3. 订单模型 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
枚举 EnumLiteral
嵌套 列表元素也是 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 演示:

  1. BeanOut + response_model 屏蔽内部字段。
  2. POST /beans 返回 201。
  3. GET /health 返回纯文本。
  4. GET /redirect 触发 302。
  5. 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-typelocationset-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

  1. 挂载 /static
  2. 渲染 index.html:欢迎语 + 当前时间。
  3. 渲染 menu.html:遍历内存菜单。
  4. 返回 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

  1. POST /upload 单文件上传(菜谱 PDF)。
  2. POST /upload-multiple 多文件上传(多张豆图)。
  3. GET /list 列出已上传文件。
  4. GET /download/{name} 下载文件。
  5. 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

  1. 定义 BusinessError + 错误码常量。
  2. 注册三种处理器:业务错误 / 参数校验 / 兜底。
  3. 演示:找不到订单、库存不足、字段错误、服务异常。

七、运行验证

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.comb.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

  1. 计时中间件(写入响应头)。
  2. 请求 ID 中间件。
  3. 简单访问日志中间件。
  4. CORS 中间件。
  5. 健康检查 + 一个示例业务接口。

七、运行验证

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

  1. get_settings() 提供配置。
  2. get_clock() 提供当前时间(演示依赖嵌套)。
  3. Pagination 类作为依赖。
  4. get_order_service() yield 模拟数据库连接。
  5. 三个业务路由使用上述依赖。

七、运行验证

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

  1. 员工清单(用户名 + bcrypt 哈希)。
  2. HTTPBasic 保护 /admin/orders
  3. APIKey 保护 /internal/stats
  4. Bearer 保护 /api/secret
  5. 公开接口:/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

  1. 内存员工表 + bcrypt 哈希密码。
  2. POST /login:校验密码 → 签发 JWT。
  3. GET /me:解析 Bearer Token,返回当前用户。
  4. POST /admin/orders:员工后台,依赖 get_current_user
  5. 公开接口 /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/

  1. db.py 引擎 + Session + Base。
  2. models.py CoffeeBean / Order / OrderItem。
  3. schemas.py 输入输出模型。
  4. crud.py 业务函数。
  5. routers/ 路由。
  6. 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 章 同步和异步接口

defasync 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

  1. GET /syncdef + time.sleep(0.2),阻塞。
  2. GET /asyncasync def + await asyncio.sleep(0.2),非阻塞。
  3. GET /io-async:用 httpx.AsyncClient 调用外部 HTTP(演示真正异步 IO)。
  4. GET /healthz:基准。
  5. 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 计算 都可(线程池)

六、踩坑点

  1. async def 里调用同步阻塞库 → 整个事件循环卡死
  2. 想"看似异步"地调用同步库:用 await run_in_threadpool(...)
  3. 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/

  1. db.py 异步引擎。
  2. models.py 复用第 16 章定义。
  3. routers/beans.py 异步 CRUD。
  4. routers/external.py 异步 HTTP(httpx)。
  5. 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。本章主要是思路与代码示例。

一、三座大山

  1. 线程/协程模型:阻塞 vs 非阻塞。
  2. 数据库连接池:连接数 ≠ QPS。
  3. 外部依赖限流: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

  1. 启动时输出事件循环 / HTTP 解析器。
  2. 演示三种 IO 模式(同步 / 异步 / 异步并发)。
  3. 信号量限制并发。
  4. 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/

  1. main.py:带 logging 中间件 + 自定义 OpenAPI。
  2. 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-headersrequest.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

八、生产前清单

  • SECRETJWT_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!


相关推荐
学废了wuwu1 小时前
从入门到实战:深入浅出模型压缩三大核心——剪枝、彩票假设与知识蒸馏
人工智能·算法·剪枝
十八岁牛爷爷1 小时前
Linux 进程与进程状态・三层级深度解析
linux·运维·服务器
Microvision维视智造1 小时前
从“看见“到“看懂“——工业视觉的大模型时刻到了吗?
人工智能·计算机视觉·视觉检测·机器视觉
janeboe1 小时前
抖音黑科技兵马俑总站源头简博科技 | 抖音锚点不合规挂载新规今日生效,短视频引流直播间迈入三端全链路监管时代
大数据·人工智能·科技
卷无止境1 小时前
Python 异常处理:从入门到工程实践
后端·python
lialaka1 小时前
「未来星途(Future_Odyssey)」——全语音_3D_AI_具身交互智能数字教官与全双工星际科普沉浸舱[1]
人工智能·3d·交互
hongmai6668881 小时前
ESP32-C61-WROOM-1-N8R2:Wi-Fi 6与RISC-V融合的中坚力量
人工智能·单片机·嵌入式硬件·物联网·智能家居·risc-v
garuda herb1 小时前
生成专题页Blog--建立并返回 MySQL 数据库连接
python·mysql
正在走向自律1 小时前
用豆包Seed Evolving打造全功能【AI智能记账】小程序,开源可落地
人工智能·小程序·开源·智能记账