FastAPI从零入门:类型注解、参数校验与自动文档实战

📅 发布时间:2026/8/30 18:51:08
FastAPI从零入门:类型注解、参数校验与自动文档实战
做过 Web 接口开发的同学多少都经历过这样的场景用 Flask 写接口路由简单上手快但参数校验基本靠手写请求体格式一复杂代码就开始膨胀用 Django 写接口功能齐全但框架较重一个小工具项目也要加载一整套体系。除此之外还有一个更隐蔽的问题——接口文档。很多团队要么靠手工维护 Markdown 文档要么用 Postman 导出一份 JSON一旦接口改了文档很难同步更新。第一次接触 FastAPI 的时候最直观的感受就是它把“参数校验、数据序列化、自动文档”这几件高频重复的事直接做进了框架层。你只需要用 Python 类型注解描述接口的入参和出参框架就会自动生成交互式文档并在请求进入处理函数之前完成校验。这个体验对习惯从零搭建接口的人来说确实是一次效率上的明显提升。本文会从环境安装开始带着你完整跑通一个 FastAPI 项目覆盖核心概念、路径参数、请求体、响应模型和实际接口开发适合零基础读者按步骤跟着操作。1. FastAPI 到底是什么1.1 从开发痛点说起Web 接口开发看起来简单实际做起来琐碎点很多。拿一个最常见的“创建用户”接口举例你要做的事包括接收 JSON 请求体、判断字段是否存在、校验邮箱格式、校验年龄范围、把数据写入数据库、把结果转成 JSON 返回然后再手工写一份接口文档给前端同事。这些工作里真正属于业务逻辑的可能只有“写入数据库”和“返回结果”其余全是在做重复的参数处理。更麻烦的是每个接口都要重复一遍项目一多代码里全是if not request.json.get(name)这类样板代码。FastAPI 解决的核心问题就是把这些重复劳动从业务代码中剥离出来交给框架层自动完成。1.2 FastAPI 的核心特点FastAPI 是一个基于 Python 类型提示Type Hints的现代 Web 框架底层依赖 Starlette 提供 ASGI 服务能力并用 Pydantic 完成数据校验和序列化。拆开来看Starlette 负责处理网络请求、路由、中间件、WebSocket 等底层能力Pydantic 负责把请求体解析成 Python 对象同时完成类型校验FastAPI 把两者整合起来对外提供一套简洁的路由和依赖注入接口。这种架构带来的实际好处主要体现在四个方面。第一是开发效率高只要把类型写清楚校验、转换、文档全部自动完成不需要再为每个接口手写参数判断。第二是性能不错基于 ASGI 异步机制同时支持同步和异步路由在大多数业务场景下性能足够用。第三是文档自带每个接口都有 Swagger UI/docs和 ReDoc/redoc可以直接调试接口改完文档自动同步。第四是生态完整依赖注入、安全认证、WebSocket、后台任务、文件上传这些常用能力都有官方支持。1.3 适合人群与学习收益本文面向零基础读者。这里的“零基础”是指之前没有用过 FastAPI但最好对 Python 基础语法有一定了解。如果你已经掌握 Python 的循环、函数、字典和列表操作就可以跟着本文动手实践。学完本文你将掌握在本地安装 FastAPI 和 Uvicorn并跑起第一个接口理解路径参数、查询参数、请求体的区别和用法使用 Pydantic 模型做数据校验和响应模型搭建一个带增删改查的小型接口项目掌握常见报错的排查思路和工程化建议。2. 环境准备与安装2.1 版本说明FastAPI 依赖 Python 3.8 及以上版本官方目前建议使用 Python 3.10 或更高版本因为新版本对类型注解的支持更完整尤其是int | None这类写法在低版本 Python 里会直接报错。本文示例以常见环境为例重点演示配置思路。你可以在终端执行python --version查看当前版本。如果版本低于 3.8建议先升级 Python。Windows 用户可以从官网下载安装包macOS 用户推荐使用 HomebrewLinux 用户可以用 apt 或 pyenv 管理多个版本。这里不展开具体安装过程但升级后一定要确认pip也指向了新版 Python否则会出现“明明装了 Python但 pip 装到的包找不到”的奇怪问题。2.2 安装 FastAPI 和 UvicornFastAPI 本身只提供框架核心实际运行还需要一个 ASGI 服务器。官方推荐 Uvicorn。在终端执行pip install fastapi uvicorn[standard]uvicorn[standard]表示安装 Uvicorn 的标准扩展包包含 uvloop、httptools 等组件性能更好也方便后续开发中用到 WebSocket 等功能。如果你的网络环境下载较慢可以换用国内镜像源pip install fastapi uvicorn[standard] -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后可以查看版本确认是否成功pip show fastapi uvicorn如果显示版本信息说明安装完成。需要注意FastAPI 的 API 在 Pydantic v1 和 v2 之间有差异本文示例基于 Pydantic v2 编写后续章节会特别说明容易踩坑的地方。2.3 项目目录规划FastAPI 这种轻量框架项目结构可以很灵活。为了后续扩展方便这里给出一个最简但合理的目录规划fastapi-demo/ ├── main.py # 应用入口 ├── routers/ # 路由模块按业务拆分成不同文件 ├── models/ # 数据模型定义 ├── schemas/ # 请求/响应模型定义 ├── dependencies/ # 依赖注入实现 ├── config.py # 配置信息 └── requirements.txt # 依赖清单对于零基础入门阶段先不要急着建这么多目录。本文前几章用单文件main.py演示到第六章实战部分再按上面的思路重组结构。3. 第一个 FastAPI 应用3.1 编写最小程序在项目目录下新建main.py输入以下内容# 文件路径fastapi-demo/main.py from fastapi import FastAPI app FastAPI(title我的第一个 FastAPI 项目) app.get(/) def read_root(): return {message: Hello FastAPI}这里有两件事需要理解。第一app FastAPI(...)创建了一个 FastAPI 应用实例后续所有路由、中间件都挂在这个实例上title参数会显示在自动生成的文档标题中。第二app.get(/)是路由装饰器表示当客户端以 GET 方式访问根路径/时执行下面的read_root函数。函数返回的是一个 Python 字典FastAPI 会自动把它序列化成 JSON 响应不需要手动调用json.dumps。3.2 启动服务在终端执行uvicorn main:app --reload解释一下这条命令。main是文件名app是 FastAPI 实例变量名这两者必须严格对应--reload表示开启热重载代码保存后服务会自动重启开发阶段非常方便。启动成功后终端会输出类似下面的信息INFO: Uvicorn running on http://127.0.0.1:8000 INFO: Application startup complete.这时打开浏览器访问http://127.0.0.1:8000页面会显示{message:Hello FastAPI}。第一次看到自己的接口返回结果说明你本地的 FastAPI 开发环境已经完整跑通了。需要提醒的是uvicorn main:app一定要在main.py所在目录下执行否则会报ModuleNotFoundError。3.3 自动生成的交互式文档FastAPI 最具吸引力的功能之一就是自动文档。在服务启动的情况下访问两个地址http://127.0.0.1:8000/docsSwagger UI 风格可以在页面上直接调用接口http://127.0.0.1:8000/redocReDoc 风格更偏阅读型文档。页面上的接口列表、参数说明、响应结构都是由 FastAPI 根据函数签名和类型注解自动生成的。你后面每加一个路由文档都会同步更新这比手工维护接口文档省事得多。很多团队第一次用 FastAPI就是从“发现文档不用自己写了”开始上瘾的。4. 路径参数、查询参数与请求体在 Web 接口开发中参数来源基本有三种路径参数、查询参数和请求体。很多新手分不清它们的区别这里放在一起讲解。4.1 路径参数路径参数是 URL 路径中带参数的一种方式例如http://127.0.0.1:8000/users/123其中123就是要传入的用户 ID。在 FastAPI 中直接在路由字符串里用花括号声明参数名并在函数参数里声明同名变量即可from fastapi import FastAPI app FastAPI() app.get(/users/{user_id}) def get_user(user_id: int): return {user_id: user_id, message: 查询成功}这里的关键是user_id: int这个类型注解。FastAPI 会根据它自动把路径中的字符串123转换为整数123转换成功后才把值传给函数。如果客户端访问/users/abcFastAPI 会返回 422 校验错误而不是把字符串传给函数这样就从源头避免了“字符串当整数用”这类低级 bug。422 Unprocessable Entity是 FastAPI 参数校验失败的默认响应这是新手最容易困惑的状态码。它说明请求本身可以被服务器理解但参数不满足接口声明的类型或约束条件属于客户端问题而不是服务端异常后面第七章会专门讲怎么排查。4.2 查询参数查询参数是 URL 中?后面携带的键值对例如http://127.0.0.1:8000/items?page1size20常用于列表分页、筛选等场景。在 FastAPI 中函数参数只要不属于路径参数就会被自动识别为查询参数app.get(/items) def list_items(page: int 1, size: int 20): return {page: page, size: size}访问/items时page和size会使用默认值得到{page: 1, size: 20}访问/items?page2size10时得到{page: 2, size: 10}。查询参数是否必填取决于有没有默认值。没有默认值的查询参数是必填的例如下面的keywordapp.get(/search) def search(keyword: str): return {keyword: keyword}这时如果访问/searchFastAPI 会返回 422 错误提示缺少keyword参数。在实际项目中列表接口通常会为分页参数设置默认值为了避免用户传负数或超大值还可以结合Query做范围校验这部分会在实战章节演示。4.3 请求体与 Pydantic 模型当参数较多且结构复杂时把它们放在请求体Request Body中更合适。比如创建订单时一次要传商品列表、收货地址、备注等多个字段用查询参数会很别扭。FastAPI 使用 Pydantic 模型来声明请求体结构先看一个最简单的例子from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class Item(BaseModel): name: str price: float is_available: bool True app.post(/items) def create_item(item: Item): return {name: item.name, price: item.price, is_available: item.is_available}这里定义了一个Item类继承自BaseModel。类中的属性就是请求体字段类型注解就是校验规则。当客户端 POST/items时FastAPI 会先解析请求体 JSON再按照Item模型做类型校验和默认值填充校验通过后把完整对象作为参数传入函数你在函数里拿到的item是一个已经校验过的 Python 对象。请求示例{ name: 键盘, price: 199.0 }响应示例{ name: 键盘, price: 199.0, is_available: true }注意is_available在请求体里没传但模型里有默认值True所以校验通过并自动填充。反过来如果客户端把price传成字符串abcFastAPI 会返回 422明确提示该字段应该是浮点数。4.4 Union 类型的作用在相关热搜词里出现了fastapi union作用这里补充说明。Union是 Python 类型注解中的联合类型表示“可以是其中任意一个类型”。在 Pydantic 模型中它通常用来处理字段可能是多种类型的场景。例如一个字段允许传入字符串或数字from typing import Union class Config(BaseModel): value: Union[str, int]在 Python 3.10 中可以简写为class Config(BaseModel): value: str | int另一个常用场景是“可为空的字段”字段允许传值也允许传nullclass User(BaseModel): name: str age: Union[int, None] None这表示age字段可以是整数也可以是null不传时默认为None。在 Python 3.10 中可以写成age: int | None None。理解 Union 对阅读 FastAPI 开源项目很有帮助很多项目里会出现Optional与Union[..., None]混用的写法。实际上从 Python 3.10 开始官方推荐优先使用X | None这种更简洁的写法。5. 响应模型与数据校验5.1 response_model 的用法除了请求体校验FastAPI 还支持响应模型。简单说就是声明“接口返回的数据结构”框架会自动过滤掉不在模型中的字段并做类型转换。看一个典型场景from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class ItemIn(BaseModel): name: str price: float secret: str class ItemOut(BaseModel): name: str price: float app.post(/items, response_modelItemOut) def create_item(item: ItemIn): return item在这个例子中请求体允许传入name、price、secret三个字段但响应只返回name和pricesecret字段会被自动过滤掉。这种“入参和出参模型分离”的做法在真实项目里非常实用。比如数据库表中可能存了密码、内部状态码等敏感字段不加过滤直接序列化返回给前端很容易造成数据泄露。用response_model可以明确声明对外暴露的边界。5.2 数据校验规则Pydantic 除类型校验外还支持更细的约束。例如用Field设置字段长度范围和数值范围from pydantic import BaseModel, Field class Item(BaseModel): name: str Field(min_length1, max_length50) price: float Field(gt0, le10000)min_length和max_length限制字符串长度gt表示大于le表示小于等于。如果客户端传入的price是负数FastAPI 会返回 422 错误提示该字段不满足约束条件。这样参数验证逻辑就从业务代码中剥离出来收敛在模型层写起来干净测试也好覆盖。实际开发中常见的日期范围、枚举值、正则表达式匹配等约束Pydantic 都支持建议在建模阶段就把校验规则想清楚。5.3 接口返回格式统一实际项目里前后端联调最怕的就是各接口返回结构不一致。有人返回{status: 1, data: ...}有人返回{code: 200, message: ok, result: ...}前端每接一个接口都要适配一次很容易出错。比较常见的做法是自定义一个统一响应模型结合 FastAPI 的response_model可以这样写from typing import Any from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class ApiResponse(BaseModel): code: int 0 message: str success data: Any None app.get(/hello, response_modelApiResponse) def hello(): return ApiResponse(data{text: Hello FastAPI})这样所有接口都返回{code, message, data}三件套。code0表示成功非 0 表示各种业务错误码。data字段用Any类型可以容纳字典、列表、字符串等任意数据。这种模式在团队协作中能显著降低前端对接成本也是很多企业项目的标准做法。本文后面实战部分也会沿用这个模式。6. 实战一个简单的待办事项管理接口前面几章都是零散的知识点这一章把它们串起来做一个完整的待办事项管理接口。功能包括创建待办事项、查询待办事项列表、查询单个待办事项、更新待办事项、删除待办事项。为了简化这里先用内存列表存储数据不接入数据库这样你可以把注意力集中在 FastAPI 本身。6.1 需求与功能拆分先明确接口设计方法路径功能POST/todos创建待办事项GET/todos查询列表支持分页GET/todos/{todo_id}查询单个待办事项PUT/todos/{todo_id}更新待办事项DELETE/todos/{todo_id}删除待办事项6.2 创建项目结构todo-demo/ ├── main.py # 应用入口和路由 ├── models.py # Pydantic 模型 └── requirements.txt核心代码分两个文件。先写models.py定义请求和响应的数据结构。6.3 编写核心代码# 文件路径todo-demo/models.py from typing import Optional from pydantic import BaseModel, Field class TodoCreate(BaseModel): title: str Field(min_length1, max_length100) done: bool False class TodoUpdate(BaseModel): title: Optional[str] Field(defaultNone, min_length1, max_length100) done: Optional[bool] None class Todo(BaseModel): id: int title: str done: boolTodoCreate用于创建接口的请求体TodoUpdate用于更新接口。注意TodoUpdate里所有字段都带默认值None表示“只更新传入的字段”没有传的字段不会被修改。再写main.py# 文件路径todo-demo/main.py from fastapi import FastAPI, HTTPException, Query from models import Todo, TodoCreate, TodoUpdate app FastAPI(title待办事项管理接口) # 模拟数据库存储 todos_db {} next_id 1 def get_next_id(): global next_id current next_id next_id 1 return current app.post(/todos, response_modelTodo, status_code201) def create_todo(todo: TodoCreate): todo_id get_next_id() item Todo(idtodo_id, **todo.model_dump()) todos_db[todo_id] item return item app.get(/todos, response_modellist[Todo]) def list_todos(page: int Query(1, ge1), size: int Query(20, ge1, le100)): items list(todos_db.values()) start (page - 1) * size end start size return items[start:end] app.get(/todos/{todo_id}, response_modelTodo) def get_todo(todo_id: int): item todos_db.get(todo_id) if item is None: raise HTTPException(status_code404, detail待办事项不存在) return item app.put(/todos/{todo_id}, response_modelTodo) def update_todo(todo_id: int, payload: TodoUpdate): item todos_db.get(todo_id) if item is None: raise HTTPException(status_code404, detail待办事项不存在) data payload.model_dump(exclude_unsetTrue) updated item.model_copy(updatedata) todos_db[todo_id] updated return updated app.delete(/todos/{todo_id}) def delete_todo(todo_id: int): if todo_id not in todos_db: raise HTTPException(status_code404, detail待办事项不存在) del todos_db[todo_id] return {message: 删除成功}代码中有几个关键点需要说明。todo.model_dump()是 Pydantic v2 的方法用于把模型转换为字典。Pydantic v1 中对应的是dict()如果你用的是 v1需要改回来。list[Todo]这种写法要求 Python 3.9如果你还在用旧版本需要改为from typing import List并用List[Todo]。exclude_unsetTrue表示只返回客户端显式传入的字段这样在更新接口里不会把未传的字段覆盖为None这是实现“部分更新”的关键。model_copy(updatedata)是 Pydantic v2 的模型复制方法v1 中对应copy(updatedata)。HTTPException用于抛出标准的 HTTP 错误配合status_code404可以返回 404 状态码和错误说明。6.4 运行与验证启动服务uvicorn main:app --reload打开http://127.0.0.1:8000/docs在 Swagger UI 里依次测试POST/todos创建两条待办事项GET/todos查看列表GET/todos/1查看 id 为 1 的事项PUT/todos/1修改事项标题或完成状态DELETE/todos/1删除事项。也可以直接用curl测试创建接口curl -X POST http://127.0.0.1:8000/todos \ -H Content-Type: application/json \ -d {title: 学习 FastAPI, done: false}预期输出{id:1,title:学习 FastAPI,done:false}再测试分页参数。如果只创建了两条数据访问http://127.0.0.1:8000/todos?page1size10会返回包含两条记录的列表访问page2则会返回空列表因为第一页已经把全部数据取完了。这个分页逻辑虽然简单但已经足够说明查询参数的默认值和范围校验是怎么工作的。6.5 结果说明到这里一个具备增删改查能力的接口服务就完成了。POST /todos返回 201 状态码表示资源创建成功GET /todos支持分页查询GET /todos/{todo_id}、PUT /todos/{todo_id}、DELETE /todos/{todo_id}在目标不存在时返回 404。实际项目中可以把todos_db替换成数据库查询核心的模型和路由结构基本可以保留。这也体现了 FastAPI 分层清晰、替换成本低的特点。一个值得注意的小细节是GET /todos/{todo_id}里声明的todo_id: int会自动完成字符串到整数的转换。如果你希望约束 id 必须大于 0可以改写为todo_id: int Path(..., ge1)这样负数 id 在进入函数前就会被拦截。真实项目中对路径参数做范围约束能提前挡住很多非法请求。7. 常见问题与排查思路新手在使用 FastAPI 时有几个高频问题整理成表格方便对照排查。问题现象常见原因解决思路启动报错ModuleNotFoundError: No module named uvicorn没有安装 Uvicorn执行pip install uvicorn[standard]访问接口返回 422参数类型不匹配或必填参数缺失打开/docs查看参数要求核对类型和默认值修改代码后服务没生效启动时未加--reload开发环境使用uvicorn main:app --reload接口 500 报错代码运行时异常比如 KeyError、TypeError查看终端日志定位具体错误行Pydantic 报model_dump不存在使用了 Pydantic v1v1 中使用dict()或升级到 v2中文乱码终端编码不是 UTF-8Windows 下执行chcp 65001切换编码POST 请求返回 405路由方法不对比如用 GET 访问了 POST 接口核对装饰器是app.post不是app.get这里面最值得多说一句的是 422 错误。FastAPI 默认在参数校验失败时返回 422响应体里会包含detail数组里面有具体的错误位置和规则描述。很多人第一次看到 422 会以为服务挂了实际上它只是表示“请求参数不合法”属于客户端问题而不是服务端异常。排查 422 时可以优先看响应体里的detail它通常长这样{ detail: [ { type: int_parsing, loc: [path, user_id], msg: Input should be a valid integer, unable to parse string as an integer, input: abc } ] }loc说明错误出在路径参数user_id上msg说明传入的字符串无法解析为整数input显示客户端实际传的值。定位起来非常快。建议你遇到 422 时第一反应不是去翻代码而是先看响应体里的detail大部分问题能直接找到答案。8. 最佳实践与工程建议入门之后真正要把 FastAPI 用到实际项目中建议关注下面几个方向。8.1 项目结构规范单文件main.py适合演示不适合长期维护。稍微复杂的项目建议按业务拆分app/ ├── main.py # 应用入口 ├── core/ │ └── config.py # 配置管理 ├── api/ │ └── v1/ │ ├── todos.py # 待办事项路由 │ └── users.py # 用户路由 ├── models/ # 数据模型 ├── schemas/ # 请求/响应模型 ├── services/ # 业务逻辑层 └── dependencies/ # 依赖注入路由通过APIRouter组织然后在main.py中注册# app/api/v1/todos.py from fastapi import APIRouter router APIRouter(prefix/todos, tags[待办事项]) router.get() def list_todos(): return []# app/main.py from fastapi import FastAPI from app.api.v1.todos import router as todos_router app FastAPI() app.include_router(todos_router)prefix统一设置路由前缀tags用于在文档中分组。接口多了之后文档里每个模块都有独立分组可读性会好很多。同时把 schemas 与 models 分开请求响应结构和数据库结构解耦后续调整数据结构时不会互相影响。8.2 异常处理与日志不要把异常处理散落在每个路由里。可以使用全局异常处理器统一处理已知异常和未预期异常from fastapi import FastAPI, Request from fastapi.responses import JSONResponse app FastAPI() class BizException(Exception): def __init__(self, code: int, message: str): self.code code self.message message app.exception_handler(BizException) async def biz_exception_handler(request: Request, exc: BizException): return JSONResponse( status_code200, content{code: exc.code, message: exc.message, data: None}, )这样业务代码里只需要raise BizException(code1001, message参数错误)前端拿到的还是统一返回结构。至于日志建议至少记录请求时间、路径、耗时和异常堆栈。可以用 Python 标准库logging或集成日志服务重点保证生产环境能根据日志快速定位问题。一个很实用的做法是在中间件里统一记录耗时超过阈值的请求单独告警。8.3 安全边界与权限如果你需要给接口加权限验证FastAPI 提供了依赖注入机制。一个常见的做法是登录后生成 token后续请求在 Header 中携带 token依赖函数负责校验。from fastapi import Depends, HTTPException, Header def verify_token(authorization: str Header(...)): if authorization ! Bearer my-secret-token: raise HTTPException(status_code401, detail未授权) return authorization app.get(/protected) def protected(auth: str Depends(verify_token)): return {message: 你有权限访问这个接口}Depends(verify_token)表示在进入路由函数前先执行verify_token。如果校验不通过直接返回 401。这比在每个路由里手写校验代码要