适用读者:要写第一个 Python API,且希望"少写校验代码"的人。
前置:S10-B03。
实测环境:FastAPI 0.162.0 / uvicorn 0.54.0 / Python 3.12.3(2026-11-05 真实启动 + curl)。
覆盖旧文:A-195(FastAPI 入门教程,292 点)。
覆盖扩展:E-S10-005(Pydantic 校验与 422)、E-S10-006(OpenAPI 由类型推导)。
上一篇:S10-B03 · 虚拟环境全生命周期:创建/激活/删除/导出
下一篇:S10-B05 · Flask 路由与错误页。
1. 一句话结论
FastAPI 的核心不是"快",是"类型标注即契约":你写 name: str,框架替你做校验、生成文档、生成 OpenAPI。
| 你写的 | 框架自动得到 |
|---|---|
| 函数签名类型 | 请求校验(422 + 明细) |
| Pydantic 模型 | 请求体 schema |
| 返回类型 | 响应 schema |
| 全部路径 | /openapi.json 与 /docs |
2. 最小可运行应用(实测代码)
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
app = FastAPI(title="S10 Demo", version="1.0.0")
class Item(BaseModel):
name: str = Field(min_length=1, max_length=20)
price: float = Field(gt=0)
tags: list[str] = []
DB: dict[int, Item] = {}
_next = 1
@app.get("/items/{item_id}")
def get_item(item_id: int, verbose: bool = Query(False)):
if item_id not in DB:
raise HTTPException(status_code=404, detail="item not found")
return {"id": item_id, "verbose": verbose, **DB[item_id].model_dump()}
@app.post("/items", status_code=201)
def create_item(item: Item):
global _next
iid = _next; _next += 1; DB[iid] = item
return {"id": iid, **item.model_dump()}
@app.get("/health")
def health():
return {"ok": True}
启动:
$ uvicorn app:app --port 19060 --log-level warning
3. 实测:校验到底有多细(E-S10-005)
3.1 正常路径
$ curl -s http://127.0.0.1:19060/health
{"ok":true}
$ curl -s -X POST http://127.0.0.1:19060/items -H 'Content-Type: application/json' \
-d '{"name":"apple","price":9.9,"tags":["fruit"]}'
{"id":1,"name":"apple","price":9.9,"tags":["fruit"]}
3.2 非法入参:422 + 逐字段明细
$ curl -s -X POST .../items -d '{"name":"","price":-1}'
HTTP 422
{"detail":[
{"type":"string_too_short","loc":["body","name"],
"msg":"String should have at least 1 character","input":"","ctx":{"min_length":1}},
{"type":"greater_than","loc":["body","price"],
"msg":"Input should be greater than 0","input":-1,"ctx":{"gt":0.0}}
]}
三个字段就能定位问题:
| 字段 | 含义 | 用法 |
|---|---|---|
loc |
出错位置(["body","name"]) |
直接告诉前端改哪个字段 |
msg |
人能读的原因 | 展示给调用方 |
type |
机器可读的错误码 | 前端做统一映射 |
注意:detail 是数组——一次请求可能同时触发多个字段错误(实测就是两条)。
3.3 类型错误与 404
$ curl -o /dev/null -w "%{http_code}" .../items/abc
422 # 路径参数类型不符 → 校验层拦截
$ curl -o /dev/null -w "%{http_code}" .../items/999
404 # 通过校验,业务层抛 HTTPException
关键区分:422 是"请求不合法",404 是"资源不存在"。这个分离让前端能区分"我错了"还是"东西没了"。
4. 文档是自动生成的(E-S10-006)
$ curl -s http://127.0.0.1:19060/openapi.json | python -c "import json,sys; d=json.load(sys.stdin); print('title:', d['info']['title']); print('paths:', list(d['paths'])); print('Item props:', list(d['components']['schemas']['Item']['properties']))"
title: S10 Demo
paths: ['/items/{item_id}', '/items', '/health']
Item props: ['name', 'price', 'tags']
这说明:/openapi.json 里的路径与 schema 完全来自你的函数签名与 Pydantic 模型——你没有写一行文档。
| 端点 | 内容 |
|---|---|
/docs |
Swagger UI(可直接试调) |
/redoc |
ReDoc(更适合阅读) |
/openapi.json |
机器可读契约(可喂给 S02-B10 的工具化链路) |
与 S02-B10 的接口:这个 openapi.json 就是"API → 可执行工具"的输入——描述质量决定工具质量。
5. 五个易踩点
| 坑 | 说明 |
|---|---|
| 路径参数没写类型 | 默认变成 str,"abc" 也能进业务 |
Query/Body 不声明 |
参数被当成 query 还是 body 取决于默认规则,易混 |
| 同步函数跑阻塞操作 | 会占满线程池(J02 讲异步) |
HTTPException 返回结构被改 |
自定义 exception_handler 会改变 detail 结构 |
| 状态码靠默认 | 创建类接口应显式 status_code=201 |
6. 自检
- FastAPI 的校验来自哪里?为什么不用手写
if not name:? - 422 的
loc/msg/type分别有什么用? - 422 与 404 的区别是什么?
/openapi.json的内容由什么决定?- 路径参数不写类型会有什么后果?
7. 下一篇
S10-B05 · Flask 路由与错误页
下一篇对照另一个主流框架:Flask 的路由注册与自定义 404/500,并用实测说明它对"类型不匹配"的处理与 FastAPI 完全不同。
本篇属于系列《S10 · Python 语言与环境》。旧文来源:A-195;扩展点 E-S10-005/006。FastAPI 0.162.0 全部响应为 2026-11-05 实测。