第11章 · FastAPI 框架基础

不写网页,专写接口。FastAPI 用 Python 类型提示自动校验数据 + 自动生成 API 文档,前后端分离就用它。

🎯 写一个商品 API,浏览器自动看到交互式文档

关于 FastAPI

FastAPI 是 2018 年新出的 Python Web 框架,专门写 API 接口(不是渲染网页的)。它有 3 个杀手锏:

💬 什么是「接口(API)」
前几章的 Flask/Django,是把数据塞进 HTML 网页再整页发给浏览器。而「接口」只发纯数据(一段 JSON 文本),完全不管页面长什么样。
打个比方:网页是后厨做好、摆好盘端上桌的一道菜;接口是后厨直接把食材打包递出窗口——谁来取(网页、手机 App、小程序)、拿去怎么摆盘,由对方自己决定。所谓前后端分离,就是后端只管做接口、只送数据。
  • 速度快——基于 Starlette + Pydantic + 异步,性能接近 NodeJS / Go。
  • 类型提示自动校验——你声明 q: int,传来字符串就自动 422 报错,不用手写校验。
  • 自动生成 API 文档——访问 /docs 直接出现 Swagger UI,可以在浏览器里调接口。
💡 应用场景
Flask/Django 适合做完整网站(含页面)。FastAPI 适合做纯接口——给前端 Vue/React、移动 App、小程序提供数据。第15章我们就用 FastAPI 给微信小程序做后台。
1 安装

安装 FastAPI 和 uvicorn

💬 为什么要装两个包
fastapi 是框架本身(写代码用);uvicorn 是 ASGI 服务器(跑代码用)。Flask/Django 自带开发服务器,FastAPI 必须配一个外部服务器,uvicorn 是官方推荐的。
SHELL
# 一行装齐:fastapi 是框架,uvicorn[standard] 是 ASGI 服务器
$ pip3 install fastapi "uvicorn[standard]" -i https://pypi.tuna.tsinghua.edu.cn/simple

# 验证
$ python3 -c "import fastapi; print(fastapi.__version__)"
0.111.0
⚠️ 引号别忘
"uvicorn[standard]" 必须带引号。中括号在很多 shell 里是特殊字符,不加引号会报错。
预备

代码里反复出现的几个写法

下面 main.py 里,有几种 Python 写法会反复出现。它们不是 FastAPI 发明的,是 Python 自带的语法。把每一种先读成一句「人话」,后面看代码就不会卡。

@app.get("/")  装饰器——给函数「挂门牌」

💬 人话翻译
@ 开头、紧贴在函数上一行的那句,叫「装饰器」。整句读作:「下面这个函数,专门负责处理 GET / 这个网址的请求。」
就像在门上挂门牌——@app.get("/items") 等于在函数门口挂一块「GET /items 请进」的牌子;请求按门牌找到该进哪个函数。
规矩:装饰器必须紧贴函数定义,中间不能空行、不能插别的语句。

item_id: int  类型提示——写明「应该是什么类型」

💬 人话翻译
参数名后面加 : 类型,是在告诉 FastAPI「这个参数应该是什么类型」。item_id: int 读作「item_id 是一个整数」。
特别注意:: 不是赋值,= 才是赋值。写了类型,FastAPI 就替你自动做两件事——检查 + 转换:网址里传来的本是文字 "42",它帮你转成整数 42;遇到转不了的(比如 abc)就自动报错。

skip: int = 0  默认值——决定参数「必填还是可选」

💬 人话翻译
在类型后面再加 = 某个值,就给参数定了一个「默认值」。客户端没传它 → 自动用默认值,于是这个参数变可选;要是没写 = → 就是必填,不传直接报错。
像点奶茶:甜度默认「正常糖」,你不说就给正常糖(有默认值=可选);但「哪种茶底」必须自己选(没默认值=必填)。

return {...}  返回字典——自动变成 JSON

💬 人话翻译
函数里 return 一个大括号字典 {"键": 值},FastAPI 会自动把它打包成 JSON 文本发给浏览器,你不用手动转。
JSON 是前后端之间传数据的「通用文本格式」,长得几乎和 Python 字典一模一样。可以这样记:字典是你随手写的便签,JSON 是把它誊写成谁都能读懂的标准格式。

f"用户#{user_id}"  f-字符串——往文字里「填空」

💬 人话翻译
字符串前面加一个小写 f,里面 {} 中的变量就会被换成它的实际值。若 user_id 是 42,f"用户#{user_id}" 就得到字符串 "用户#42"
像填空模板:f"亲爱的{名字}",把名字填进去再用。
2 第一个程序

HelloWorld + 自动 API 文档

新建文件 /home/fastapi/main.py,敲入下面这段。每一行都配了注释,对照上面的「几个写法」一起读:

PYTHON /home/fastapi/main.py
# ============ FastAPI 最小程序 ============
from fastapi import FastAPI

# 造一个「应用」对象,整个程序都挂在它身上
#(和 Flask 的 app = Flask(__name__) 一个意思)
app = FastAPI()


# 装饰器=挂门牌:GET 访问 / 时,就执行下面这个函数
@app.get("/")
def read_root():
    # return 一个字典,FastAPI 自动转成 JSON 发出去
    return {"message": "Hello, FastAPI!"}


# 网址里 {item_id} 是占位符;item_id: int 是类型提示
@app.get("/items/{item_id}")
def read_item(item_id: int):
    # 访问 /items/42:42 自动装进 item_id,并转成整数
    # 访问 /items/abc:转不成整数,自动返回 422,函数都不执行
    return {"item_id": item_id, "price": 9.99}
💬 这段代码,从上往下读一遍
第 1 行 from fastapi import FastAPI:把 FastAPI 这个工具取过来备用。
app = FastAPI():造出「应用」对象 app,后面所有接口都挂在它身上。
@app.get("/"):门牌——它下面紧挨着的 read_root 负责处理 GET /;函数 return 字典,自动变 JSON。
{item_id} + item_id: int:网址里大括号是「占位符」,访问 /items/42 时 42 被装进参数 item_id;又因为标了 : int,FastAPI 自动把文字转成整数,转不了就报 422。

启动方式不一样

FastAPI 不能直接 python main.py。要用 uvicorn 把它跑起来:

SHELL
$ cd /home/fastapi
# main 是文件名(main.py),app 是文件里的实例对象名
# --reload 改代码自动重启,--host 0.0.0.0 让外网能访问
$ uvicorn main:app --reload --host 0.0.0.0 --port 8000
INFO:     Uvicorn running on http://0.0.0.0:8000
💬 main:app 到底指什么
冒号两边分开看:main 是文件名(main.py 去掉 .py),app 是你在文件里写的那个 app = FastAPI() 变量名。合起来=「去 main.py 里找名叫 app 的应用,把它跑起来」。哪天文件名或变量名改了,这里要跟着改。

测试三个 URL

GET http://IP:8000/ → {"message": "Hello, FastAPI!"}
GET http://IP:8000/items/42 → {"item_id": 42, "price": 9.99}
GET http://IP:8000/docs ★ 自动生成的交互式文档

🎉 重头戏:自动 API 文档

访问 /docs,看到的就是 Swagger UI——所有接口列表 + 在线试用

点开任一接口 → "Try it out" → 填参数 → "Execute",不用 Postman 也能测

💡 还有 /redoc
/redoc 是另一种风格的文档(更适合阅读,不能在线试),/docs 适合开发调试。两个同时存在,按需用。
3 Path & Query

路径参数 vs 查询参数

💬 一图看清两种参数
http://api.com/users/42?page=2&size=10
                   ↑                ↑
              Path 路径参数     Query 查询参数
              (路径的一部分)   (问号后面 key=value)

规则:函数参数到底算哪种

FastAPI 靠一个简单规则区分:看参数名有没有在网址的 {} 里出现过

你写的参数FastAPI 当成
名字在 {} 里出现过Path 参数(网址路径的一部分)
名字没在 URL 里出现Query 参数(问号后面的 key=value)
带默认值(如 = 0该参数可选
不带默认值该参数必填

追加到 main.py

PYTHON main.py(追加)
# ===== Path 参数:名字出现在网址的 {} 里 =====
@app.get("/users/{user_id}")
def get_user(user_id: int):
    # user_id 在 {} 里 → Path 参数;: int → 自动校验+转换
    return {"user_id": user_id, "name": f"用户#{user_id}"}


# ===== Query 参数:名字没出现在网址里 =====
@app.get("/articles/")
def list_articles(skip: int = 0, limit: int = 10):
    # skip、limit 没在 {} 里 → Query 参数
    # 都有默认值 → 可选;不传就是 0 和 10
    # 先用一个写死的小列表当假数据
    articles = ["文章A", "文章B", "文章C", "文章D", "文章E"]
    # 列表切片:跳过 skip 个,再往后取 limit 个
    return articles[skip : skip + limit]


# ===== Path + Query 混着用 =====
@app.get("/users/{user_id}/articles")
def user_articles(user_id: int, q: str = None):
    # user_id 在 {} 里 → Path;q 不在 → Query
    # q: str = None → 可选的字符串,默认没有
    result = {"user_id": user_id}
    if q:
        result["search"] = q       # 传了 q 才加进结果
    return result
💬 articles[skip : skip + limit] 是「列表切片」
方括号里写 [起 : 止],能从列表里截出一段,规则是含起点、不含终点
比如上面这个 5 个元素的列表,articles[1:3] 取出第 1、2 个(即 文章B、文章C)。这里写成 skip : skip + limit,意思就是「跳过 skip 个,再往后拿 limit 个」——正好是翻页的逻辑。

测试一下

GET/users/42→ Path 参数
GET/users/abc→ 自动 422 错误
GET/articles/?skip=1&limit=2→ 取出 文章B、文章C
GET/users/42/articles?q=python→ Path + Query
💡 自动校验是 FastAPI 最香的特性
你只是写了 user_id: int,FastAPI 就替你做了:参数提取、类型转换、错误处理、生成文档。这一点 Flask/Django 都做不到——它们都得手写校验

📋 本节小结

这一章的命令清单

  • pip3 install fastapi "uvicorn[standard]" — 安装
  • uvicorn main:app --reload --host 0.0.0.0 — 启动
  • http://IP:8000/docs — 在线调试 API

Path 参数 vs Query 参数,一句话分清

  • 名字出现在网址 {} 里 → Path 参数(如 /users/{user_id} 的 user_id)。
  • 名字没出现在网址里 → Query 参数(问号后面的 key=value,如 ?skip=1)。
  • 有默认值 → 可选;没默认值 → 必填。

FastAPI 跟另两个框架的核心差异

  • 没有模板——FastAPI 不渲染 HTML,只返回 JSON。
  • 没有 ORM——要操作数据库自己装 SQLAlchemy / Tortoise / 直接用 PyMySQL。
  • 类型提示就是文档——你写 q: int,FastAPI 替你做:校验、转换、错误响应、文档生成。