这篇文章整理了笔者学习 FastAPI 的过程,并将其转化为一套可以按步骤操作和验证的实践教程。目标是完成两个动作:创建路由,以及正确处理请求。重点保留了学习中最容易混淆的几组边界:400 和 422、请求校验和响应校验、FastAPI 应用和 Uvicorn 服务器。
1. 先跑通一个最小服务
本文示例在以下环境中验证:FastAPI 0.116.1、Pydantic 2.8.2、Starlette 0.47.2、Uvicorn 0.35.0。版本不同可能影响个别错误信息,但不改变本文的基本处理链。
先创建虚拟环境并安装 FastAPI:
1 | python -m venv .venv |
main.py 中写一个最小的 /hello 路由:
1 | from fastapi import FastAPI |
name: str | None = None 表示:name 可以是字符串,也可以是 None;如果请求没有提供 name,就使用默认值 None。因此 GET /hello?name=Lin 时,name 是字符串 "Lin",而直接访问 GET /hello 时,name 是 None。由于它有默认值,FastAPI 会把 name 识别为可选的查询参数。
从 main.py 所在目录启动:
1 | uvicorn main:app --reload |
这里的 main:app 不是一个 URL:冒号左边是 Python 模块路径,右边是模块中的应用对象名。app = FastAPI() 创建的是应用对象,它保存路由并负责请求处理;Uvicorn 是服务器进程,负责监听端口、接收连接并调用这个应用。
访问下面两个地址,可以看到同一个路由如何读取查询参数:
1 | GET /hello |
FastAPI 应用和 Uvicorn 之间通过 ASGI(Asynchronous Server Gateway Interface)协作。ASGI 是应用与服务器之间的接口规范,不是业务路由本身。传统的 WSGI 主要面向同步调用;ASGI 为异步调用、长连接等场景提供了更合适的接口。对入门代码而言,只要先记住这三层即可:
1 | 浏览器/客户端 -> Uvicorn(监听端口) -> ASGI 应用(FastAPI 路由和业务逻辑) |
更多 ASGI 介绍,可参考这篇文章>>
2. 路由是 HTTP 方法和路径的组合
同一个路径可以注册不同的 HTTP 方法。FastAPI 选择处理函数时同时看方法和路径:
1 | from fastapi import FastAPI |
对应关系是:
| 请求 | 结果 |
|---|---|
GET /items |
执行 list_items |
POST /items |
执行 create_item |
GET /items/42 |
执行 get_item,item_id 为 42 |
GET /unknown |
404 Not Found,没有匹配的路径 |
POST /items/42 |
如果只注册了 GET,对应路径存在但方法不允许,返回 405 Method Not Allowed |
404 和 405 的差别:前者是路径没有匹配到,后者是路径匹配到了但 HTTP 方法不在注册列表中。
3. 路径参数、查询参数和校验时机
路径占位符必须和函数参数同名。没有出现在路径中的简单类型参数,FastAPI 默认把它解释为查询参数:
1 | from fastapi import FastAPI |
这里 book_id 是路径参数,detail 和 sort 是查询参数:
1 | GET /books/42 |
detail 和 sort 有默认值,所以可以省略;如果写成 q: str 而不提供默认值,GET /search 会在进入函数前返回 422:
1 |
|
GET /search?q=fastapi&limit=abc 同样返回 422,因为 limit 声明为 int,字符串 abc 无法转换为整数。
关键执行顺序是:
1 | 路由匹配 -> 参数转换和校验 -> 函数体 |
因此:
GET /books/abc能匹配路径,但abc不能转换成int,返回422,函数体不会执行。GET /books/42转换成功,函数体执行,通常返回200。
这里的 422 不是“资源不存在”。/users/abc 和 /users/999 应该分开判断:前者是参数校验失败,后者在 999 是合法整数的前提下,进入函数查询数据库,查不到用户时才是 404。
4. POST 请求体由 Pydantic 模型声明
POST 请求的 JSON body 不会因为使用了 POST 就自动变成某种结构。需要用 Pydantic 的 BaseModel 明确声明字段,再把模型类型写到路由函数参数上:
1 | from fastapi import FastAPI |
person: PersonIn 同时完成两件事:告诉 FastAPI 从请求体读取 JSON,并让 Pydantic 按 PersonIn 校验和转换字段。下面的请求会得到 200,函数收到的 person.age 是整数 20:
1 | {"name": "Lin", "age": "20"} |
下面的请求得到 422,函数体不会执行:
1 | {"name": "Lin", "age": "unknown"} |
同理,缺少 age 也会得到 422。Pydantic 默认会忽略模型没有声明的额外字段,因此 city 不会自动出现在返回值中:
1 | {"name": "Lin", "age": 20, "city": "Shanghai"} |
类型转换不是无条件的
在当前 Pydantic v2 环境中,int 字段可以把可解析的数字字符串转换成整数,但 str 字段不会把整数 123 自动转换成字符串。也就是说,下面两种行为并不对称:
1 | age: int,输入 "20" -> 通过,得到 20 |
这种不对称是有意的:目标是让运行时数据更接近接口声明,避免把本来应该是字符串的字段悄悄接收成整数。
PS:不要把一个字段的转换经验推广到所有字段,最终规则要看目标类型和 Pydantic 版本。
5. 请求校验、业务异常和依赖
FastAPI 的请求处理可以画成一条更完整的链:
1 | 客户端请求 |
先记住这条处理链即可,下一章节会详细介绍
response_model如何校验和过滤响应。
需要注意,“校验请求数据”和“解析依赖”不是两条严格串行、互不交错的流水线。FastAPI 会在调用路由函数前汇总参数并求解依赖;某个端点参数校验失败时,路由函数不会执行,但不依赖这个错误参数的依赖函数可能已经执行。
Depends 用来声明接口依赖的处理步骤。下面的依赖函数要求一个 token 查询参数:
1 | from fastapi import Depends, FastAPI, HTTPException |
这段代码的执行范围如下:
| 请求 | 状态码 | 执行情况 |
|---|---|---|
/admin/items/abc?token=admin |
422 |
require_admin 可能已执行;item_id 校验失败,路由函数不执行 |
/admin/items/1 |
422 |
token 是依赖的必填参数,依赖函数还没执行 |
/admin/items/1?token=wrong |
403 |
依赖函数执行并主动抛出异常,路由函数不执行 |
/admin/items/0?token=admin |
404 |
依赖通过,路由函数执行后主动抛出异常 |
/admin/items/1?token=admin |
200 |
依赖通过,路由函数正常返回 |
如果认证信息应该放在请求头,不要继续使用普通的 str 参数;应显式使用 Header,否则 FastAPI 会把它当作查询参数处理。
例如,把依赖函数改成下面这样,token 就会从名为 token 的请求头中读取:
1 | from fastapi import Header, HTTPException |
请求时把 token 放在请求头,而不是 URL 的查询字符串中:
1 | curl -H "token: admin" "http://127.0.0.1:8000/admin/items/1" |
Header(...) 中的 ... 表示这个请求头是必填的;如果完全不发送 token,FastAPI 会在依赖函数执行前返回 422。
6. response_model 约束最终响应
请求模型约束“函数能接收什么”,response_model 约束“接口对外返回什么”:
1 | from fastapi import FastAPI |
最终响应是:
1 | {"name": "Lin"} |
password 虽然出现在函数返回字典中,但不在 UserOut 中,所以被响应模型过滤。status_code=201 则把这次成功响应标记为资源创建成功。
响应校验发生在函数返回之后。如果声明:
1 | class ArticleOut(BaseModel): |
但函数只返回 {"title": "FastAPI 入门"},当前环境下会发生响应校验失败,客户端通常看到 500 Internal Server Error。这和请求体缺少字段不同:请求体缺字段是在函数执行前返回 422,响应缺字段是服务器没有兑现自己声明的响应契约,应按服务端错误排查。
7. 400 和 422 到底有什么不同
| 状态码 | 通用语义 | FastAPI 入门场景 |
|---|---|---|
400 Bad Request |
请求作为一个有效请求无法被服务器处理,语义范围较宽 | 应用或中间件主动判定请求报文/通用请求不合适,并显式抛出 HTTPException(400, ...) |
401 Unauthorized |
缺少或无效的认证凭证 | 没有有效登录凭证,通常还应配合 WWW-Authenticate |
403 Forbidden |
请求被理解,但当前身份不被允许 | token 校验通过请求格式、但权限不足,或依赖主动拒绝 |
404 Not Found |
路径或资源不存在 | /unknown 没有路由,或合法 user_id 查不到用户 |
405 Method Not Allowed |
路径存在,但 HTTP 方法不允许 | 只有 GET 路由,却发送 POST |
415 Unsupported Media Type |
请求体媒体类型不被支持 | 服务只接受某种 Content-Type |
422 Unprocessable Content |
内容语法可以理解,但字段语义不能按声明处理 | FastAPI 默认的路径、查询、请求头和 Pydantic 请求体校验失败 |
500 Internal Server Error |
服务端处理自身出错 | 函数返回值不符合 response_model |
为什么 FastAPI 经常返回 422
在 FastAPI 中,下面这些声明式校验失败通常都会被统一包装成 422:
user_id: int收到abc;limit: int收到abc;- Pydantic 模型缺少必填字段;
- 字段值无法转换或不满足约束;
- 在本文验证的 FastAPI 0.116.1 中,请求体 JSON 解码失败也以
422的json_invalid错误返回。
这说明 FastAPI 的默认行为确实比“语法错误一律 400”的简化口诀更具体,但不能据此把 422 当成 FastAPI 私有定义。HTTP 的通用语义仍然来自规范;框架可以选择自己的默认映射,项目也可以通过异常处理器改写映射。
400 仍然有明确用途。例如,下面的接口绕过 Pydantic 请求体模型,手动解析 JSON,并把 JSON 语法错误主动映射为 400:
1 | from json import JSONDecodeError |
这里的 400 不是 Pydantic 自动产生的,而是应用作者主动做出的接口约定。如果改用 payload: SomeModel 声明请求体,解析和字段校验会交回 FastAPI;在本文版本中,坏 JSON 和字段校验错误默认都返回 422。实际项目应在团队 API 规范中统一选择,不要让同一类错误在不同接口间随机使用 400 和 422。
8. 自动文档不是运行结果,而是接口契约
启动服务后,FastAPI 默认提供:
/docs:Swagger UI;/redoc:ReDoc;/openapi.json:OpenAPI schema 原文。
文档中的参数类型、是否必填、请求体结构和响应结构来自 Python 类型声明、Pydantic 模型、路径装饰器和 response_model。它展示的是接口的预期契约,不是某一次请求实际返回的 JSON。
例如:
1 | class BookOut(BaseModel): |
删除 response_model=BookOut 后,函数仍可能返回同样的字典,但自动文档不再明确保证 title 和 pages 这两个响应字段。运行时数据和 OpenAPI schema 是两个层次,排查文档问题时不要把它们混在一起。
9. 一个完整的小型 API
下面把前面的路由、请求模型、响应模型、依赖和异常组合起来。它也是一个适合自己复制运行的练习:
1 | from fastapi import Depends, FastAPI, HTTPException |
用下面的请求验证每一层:
1 | curl -X POST "http://127.0.0.1:8000/products/7?token=admin" \ |
预期状态码是 201,响应中只有 id、title 和 quantity,internal_cost 会被 ProductOut 过滤。再试几个故障请求:
1 | POST /products/abc?token=admin -> 422,product_id 校验失败 |
10. 遇到错误时按阶段排查
不要先盯着状态码猜原因,先问“请求走到了哪一步”:
404:确认路径是否注册;如果路径存在,再确认是不是把方法写错导致405。422:查看响应体里的detail,重点看loc是path、query、header还是body;这是声明式请求校验没有通过。- 函数没有打印或数据库查询记录:先检查校验和依赖,校验失败时函数体不会执行。
- 进入函数后得到
404:这是业务查找结果,不是参数类型错误。 - 返回
500且使用了response_model:检查函数返回字典是否缺少模型必填字段、类型是否不符合模型。 - token 明明放在请求头却仍然提示缺少参数:检查依赖参数是否声明为
Header,普通str参数默认来自查询参数。
这条排查路径比死记“某状态码代表某句话”更可靠:先定位阶段,再判断是框架默认校验、业务主动异常,还是服务器自己的响应契约出了问题。
结语
FastAPI 入门真正需要掌握的是处理链,而不是装饰器数量:@app.get 或 @app.post 注册方法和路径;函数签名声明参数来源与类型;Pydantic 校验请求体;Depends 插入认证等前置步骤;response_model 约束对外响应;OpenAPI 文档把这些声明展示成可操作的契约。
值得注意的是:/users/abc 的 422 是参数校验失败,/users/999 的 404 是资源查找失败;请求校验失败不会进入函数,而响应模型失败通常属于服务端错误。掌握这几个边界后,后续接数据库、认证和更复杂的业务逻辑,仍然可以沿着同一条处理链定位问题。