第一个FastAPI应用:创建最小API、路由定义、路径参数、查询参数、请求体解析、响应模型基础

FastAPI 之所以能在短时间内成为 Python 后端开发的热门选择,核心在于它同时解决了性能、开发效率与类型安全三大痛点。本章将从一个最小的可运行应用出发,逐步拆解路由、参数解析与响应模型的核心机制。我们不仅要学会“怎么写”,更要理解“为什么这样写”——尤其是 FastAPI 如何借助 Python 类型注解完成请求校验、数据序列化与 API 文档自动生成。这一章是后续所有高级特性(依赖注入、安全认证、数据库集成)的地基,请务必亲手敲一遍代码并观察每一步的交互行为。

HTTP 客户端 浏览器 / Postman 请求 FastAPI 应用 路由定义 (装饰器) 路径/查询/请求体 类型校验与转换 响应模型序列化 业务处理函数 OpenAPI 文档 /docs 自动生成 Pydantic 模型 数据校验与序列化 Python 类型注解 int / str / BaseModel FastAPI 核心:类型注解驱动 → 自动校验 + 自动文档 + 高性能

上图展示了 FastAPI 的核心数据流:HTTP 请求进入路由层后,由参数解析层根据类型注解完成校验与转换,业务函数处理后,响应模型层负责序列化输出。右侧三个模块(OpenAPI 文档、Pydantic 模型、类型注解)并非独立存在,而是由同一套类型系统驱动——这正是 FastAPI 区别于 Flask、Django 的关键设计哲学。

3.1 最小 API 的创建与运行机制

核心原理:FastAPI 应用本质上是一个 ASGI(Asynchronous Server Gateway Interface)应用实例。通过 FastAPI() 创建应用对象,再使用装饰器注册路由。最小 API 只需要一个路径操作装饰器(如 @app.get("/"))和一个返回字典或 Pydantic 模型的函数。FastAPI 会自动将返回值序列化为 JSON,并设置正确的 Content-Type 头。

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello FastAPI"}

启动命令为 uvicorn main:app --reload。其中 main 是模块文件名,app 是 FastAPI 实例变量名,--reload 开启开发模式热重载。访问 http://127.0.0.1:8000 即可看到 JSON 响应,访问 /docs 可查看自动生成的 Swagger 交互文档。

经验提醒:FastAPI 默认使用 application/json 响应格式。如果返回 dict,FastAPI 会通过 jsonable_encoder 自动处理 datetime、Decimal 等非原生 JSON 类型。建议从第一个应用开始就使用 Pydantic 模型作为响应类型,而不是裸字典,这样能获得更好的类型提示和文档支持。

3.2 路由定义:从静态到动态

操作流程:路由定义遵循“路径 + HTTP 方法”的映射规则。FastAPI 支持 @app.get()、@app.post()、@app.put()、@app.delete() 等常用装饰器,也支持 @app.api_route() 指定多个方法。路径参数使用花括号 {} 声明,并在函数签名中通过同名参数接收。

@app.get("/items/{item_id}")
def read_item(item_id: int):
    return {"item_id": item_id, "name": f"Item {item_id}"}

这里 item_id: int 的类型注解至关重要——FastAPI 会自动将路径中的字符串转换为 int,如果转换失败(如访问 /items/abc),将返回 422 校验错误,而不是让程序崩溃。路径参数的顺序不影响匹配,但路径中不能有重复的参数名。

路由模式示例匹配路径说明
静态路径/users/users精确匹配
动态路径/users/{user_id}/users/42路径参数,类型转换
多段路径/files/{file_path:path}/files/docs/report.pdf:path 转换器匹配任意多段
固定前缀/api/v1/items/api/v1/items常用于版本控制
常见误区:不要将路径参数与查询参数混淆。路径参数是 URL 中不可省略的部分(如 /users/42),而查询参数是 ? 后的键值对(如 /users?age=30)。路径参数适合标识资源,查询参数适合过滤、排序或分页。如果路径参数声明为 int 但实际传入非数字,FastAPI 会返回 422 错误,这是预期行为,不要试图捕获 ValueError 来“修复”。

3.3 查询参数:可选、默认值与类型校验

核心原理:查询参数是 URL 中 ? 后面的键值对。在 FastAPI 中,只要函数参数没有在路径中声明,就会被自动识别为查询参数。查询参数可以是必填的(无默认值)、可选的(有默认值)或可空的(Optional[type])。FastAPI 会根据类型注解自动解析、转换和校验。

@app.get("/search")
def search_items(q: str | None = None, limit: int = 10, skip: int = 0):
    return {"query": q, "limit": limit, "skip": skip}

访问 /search?q=fastapi&limit=5&skip=2 时,FastAPI 会解析出 q="fastapi"、limit=5、skip=2。如果省略 q,则默认为 None;如果 limit 传入非整数(如 limit=abc),返回 422 错误。查询参数还支持 Query 类进行更精细的控制,如最小/最大值、正则校验、别名等。

实战场景:分页与过滤
在真实项目中,查询参数常用于列表接口的分页和过滤。例如一个商品列表接口:/products?category=electronics&page=2&size=20。建议将 page 和 size 的默认值设为 1 和 20,并使用 Query(ge=1) 限制最小值为 1,避免负数或零导致的异常。对于可选的过滤条件,使用 Optional[str] 类型,这样既能保持 API 的灵活性,又能获得类型安全。

3.4 请求体解析:Pydantic 模型的力量

操作流程:当客户端需要提交结构化数据(如创建用户、更新订单)时,应使用 POST/PUT 方法并携带请求体。FastAPI 要求使用 Pydantic 模型定义请求体结构,然后在函数参数中声明该模型类型。FastAPI 会自动读取请求体 JSON,校验字段类型和约束,并将结果注入到函数参数中。

from pydantic import BaseModel, Field

class UserCreate(BaseModel):
    username: str = Field(..., min_length=3, max_length=20)
    email: str
    age: int = Field(ge=0, le=120)

@app.post("/users/")
def create_user(user: UserCreate):
    # 此时 user 已经通过校验,可以直接使用
    return {"id": 1, **user.model_dump()}

关键点:Field(..., min_length=3) 中的 ... 表示必填,min_length 和 max_length 是字符串长度约束,ge/le 是数值范围约束。如果请求体缺少必填字段或字段类型错误,FastAPI 返回 422 错误,并附带详细的错误信息(哪个字段、什么原因)。

校验场景Pydantic 写法错误示例
必填字段name: str缺少 name 字段
字符串长度Field(min_length=2, max_length=10)"a" 或 "abcdefghijk"
数值范围Field(ge=0, le=100)-1 或 101
正则匹配Field(pattern=r"^\d{3}-\d{4}$")"12345" 不匹配
可选字段nickname: str | None = None允许缺失或 null
常见误区:不要将请求体参数与查询参数混用。一个函数中只能有一个 Pydantic 模型参数作为请求体(除非使用 embed=True 或嵌套模型)。如果同时需要路径参数、查询参数和请求体,路径参数和查询参数应放在请求体参数之前声明。另外,Pydantic v2 中 .dict() 已弃用,应使用 .model_dump()。

3.5 响应模型:控制输出结构与数据安全

核心原理:响应模型(response_model)用于声明 API 返回的数据结构。它有两个核心作用:一是过滤输出字段(防止敏感数据泄露),二是生成准确的 OpenAPI 文档。FastAPI 会使用响应模型对返回值进行序列化,如果返回的字典包含响应模型中未定义的字段,这些字段会被自动过滤掉。

from pydantic import BaseModel

class UserOut(BaseModel):
    id: int
    username: str
    email: str

class UserInDB(UserOut):
    hashed_password: str

@app.post("/users/", response_model=UserOut)
def create_user(user: UserCreate):
    # 假设内部创建了 UserInDB 对象
    user_db = UserInDB(id=1, username=user.username, email=user.email, hashed_password="fakehash")
    return user_db  # 响应中不会包含 hashed_password

这里 response_model=UserOut 确保即使内部返回了包含 hashed_password 的 UserInDB 对象,最终响应也只包含 id、username、email 三个字段。这是防止密码、密钥等敏感信息泄露的标准做法。

案例拆解:用户注册接口的完整闭环
背景与约束:某社交平台需要实现用户注册接口,要求用户名唯一、密码至少 8 位、邮箱格式合法。前端需要清晰的错误提示,后端需要防止密码泄露。
初始目标:设计一个 POST /users 接口,接收 JSON 请求体,返回创建成功的用户信息(不含密码)。
关键输入:请求体 {"username": "alice", "email": "alice@example.com", "password": "secret123"}。
分析判断:需要三个模型:UserCreate(请求体,含密码)、UserInDB(数据库模型,含哈希密码)、UserOut(响应模型,不含密码)。密码字段使用 Field(min_length=8) 校验。
实际操作:① 定义三个 Pydantic 模型;② 路由函数接收 UserCreate,内部将密码哈希后构造 UserInDB;③ 返回 UserInDB 对象,但声明 response_model=UserOut。
结果对比:响应体为 {"id": 1, "username": "alice", "email": "alice@example.com"},无密码字段。若请求体密码少于 8 位,返回 422 错误,错误信息指明 password 字段不满足最小长度。
风险与代价:如果忘记声明 response_model,密码将直接暴露在响应中。建议所有涉及敏感字段的接口强制使用响应模型,并在代码评审中检查。
复盘经验:响应模型不仅是文档工具,更是安全边界。将输入模型、数据库模型、输出模型分离,是 FastAPI 项目的标准分层模式。

3.6 常见误区与调试技巧

误区一:混淆路径参数与查询参数。路径参数是 URL 的一部分,查询参数是 ? 后的键值对。如果参数在路径中声明,就不能在函数签名中作为查询参数使用。

误区二:忽略类型注解。FastAPI 完全依赖类型注解进行校验和序列化。如果函数参数不写类型注解,FastAPI 会将其视为查询参数,且不进行任何类型转换。例如 def read_item(item_id): 中的 item_id 会被当作字符串处理。

误区三:请求体与查询参数混用。一个函数中只能有一个 Pydantic 模型作为请求体。如果尝试声明两个模型参数,FastAPI 会报错。解决方案是使用嵌套模型或 embed=True。

调试技巧:当遇到 422 错误时,FastAPI 的响应体包含 detail 数组,其中每个元素包含 loc(错误位置)、msg(错误信息)、type(错误类型)。例如 {"detail":[{"loc":["body","age"],"msg":"Input should be greater than or equal to 0","type":"greater_than_equal"}]}。在开发阶段,建议直接访问 /docs 页面进行交互式调试,FastAPI 会生成可执行的请求示例。

总结复盘:本章的核心是理解 FastAPI 的“类型驱动”开发模式。路径参数、查询参数、请求体、响应模型,本质上都是通过类型注解声明契约,由 FastAPI 自动完成解析、校验、序列化。建议读者完成以下练习:① 创建一个包含路径参数和查询参数的 GET 接口;② 创建一个包含嵌套 Pydantic 模型的 POST 接口;③ 使用 response_model 过滤敏感字段。这三个练习覆盖了本章 80% 的知识点,完成后即可进入下一章学习更高级的依赖注入与数据库集成。