FastAPI 基础篇:类型注解驱动的 Python Web 开发范式

FastAPI 基础篇:类型注解驱动的 Python Web 开发范式

1. 引子:写 API 为什么这么累?

先问一个问题:用 Flask 或 Django 写一个带参数校验和文档的 POST 接口,需要写多少代码?

python 复制代码
# Flask 写一个带校验的 POST 接口
from flask import Flask, request, jsonify
from marshmallow import Schema, fields, ValidationError

app = Flask(__name__)

class BookSchema(Schema):
    title = fields.String(required=True, validate=validate.Length(min=2, max=50))
    price = fields.Float(required=True, validate=validate.Range(min=0))

@app.route('/books', methods=['POST'])
def create_book():
    # 手动解析 JSON
    data = request.get_json()

    # 手动校验
    schema = BookSchema()
    try:
        book = schema.load(data)
    except ValidationError as e:
        return jsonify({'errors': e.messages}), 422

    # 业务逻辑
    return jsonify({'title': book['title'], 'price': book['price']})

# 还没写文档呢......

这份代码暴露了传统框架的典型痛点:

  • 手动解析请求体
  • 手动定义校验规则(且校验和路由是分离的)
  • 手动维护 API 文档(或者额外装 flasgger/Swagger)
  • 手动序列化响应

如果每个接口都这样写,项目中 30% 的代码都在做"参数搬运"。更要命的是,校验代码、文档注释、业务逻辑散落在三个地方,修改一个字段要改三处。

FastAPI 解决这个问题的思路很激进:你只需要写 Python 类型注解,剩下的框架替你搞定。

python 复制代码
# FastAPI 做同样的事
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class BookIn(BaseModel):
    title: str = Field(..., min_length=2, max_length=50)
    price: float = Field(..., gt=0)

@app.post('/books')
async def create_book(book: BookIn):
    return {'title': book.title, 'price': book.price}

做完了。校验、文档、序列化全部自动完成,打开 http://127.0.0.1:8000/docs 就能看到交互式文档。


2. 核心概念:FastAPI 凭什么能这么干?

FastAPI 不是凭空冒出来的东西,它的能力建立在三个技术支柱上:

FastAPI = Starlette(异步网络能力) + Pydantic(数据校验) + 类型注解驱动

2.1 ASGI 与异步

在 FastAPI 之前,Python Web 框架基本都跑在 WSGI(Web Server Gateway Interface)上,这是 Python 在 2003 年提出的标准。WSGI 的工作方式是:来一个请求,开一个线程,处理完再响应------同步阻塞

ASGI(Asynchronous Server Gateway Interface)是 2016 年提出的新一代标准,它允许服务器在处理一个请求的同时,去处理另一个请求。这对 IO 密集型场景(数据库查询、外部 API 调用、文件读写)提升巨大。

对比 WSGI(Flask/Django 传统模式) ASGI(FastAPI/Starlette)
处理方式 同步阻塞,一个请求占一个线程 异步非阻塞,事件循环调度
并发能力 靠多线程,线程切换开销大 靠协程,轻量级切换
长连接 不支持 WebSocket 原生 原生支持 WebSocket/SSE
性能 中等 高(接近 Node.js/Go 的水平)

FastAPI 跑在 Starlette 之上,Starlette 是一个轻量级的 ASGI 框架/工具包,提供了路由、中间件、WebSocket 等底层能力。FastAPI 在 Starlette 之上加了一层类型注解驱动的 API 层,这才是它的核心竞争力。

2.2 类型注解驱动

Python 的类型注解(Type Hints)从 Python 3.5 开始引入,最初只是为了给 IDE 做静态检查。FastAPI 把这个特性玩出了新高度:类型注解不再是"注释",而是框架行为的"指令"。

python 复制代码
@app.get('/items/{item_id}')
async def read_item(
    item_id: int,                          # 路径参数,自动转 int
    q: str | None = None,                   # 查询参数,可选
    book: BookIn,                           # 请求体,自动解析 JSON
    token: str = Header(None),              # 请求头
    session_id: str = Cookie(None),         # Cookie
):
    return {'item_id': item_id, 'q': q}

每一处注解都在告诉 FastAPI 三件事:

  1. 参数从哪里来(路径、查询、请求体、请求头......)
  2. 应该是什么类型(自动校验 + 转换)
  3. 是否可选(有默认值 = 可选,没有 = 必填)

2.3 Pydantic:背后的守门员

Pydantic 是 FastAPI 用来做数据校验的引擎。每当 FastAPI 接收到请求数据,都会交给 Pydantic 模型去做校验。

Pydantic 的核心机制是 BaseModel------你定义一个类,声明字段和类型,Pydantic 自动完成校验、转换和序列化:

python 复制代码
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime

class User(BaseModel):
    id: int
    name: str = Field(..., min_length=2, max_length=20)
    email: EmailStr
    created_at: datetime | None = None

# 传入的 JSON 会自动校验和转换
# {"id": "123", "name": "张三", "email": "test@example.com"}
# → id 自动从字符串 "123" 转为整数 123
# → email 格式不对直接返回 422 错误

# 输出的对象自动序列化为 JSON
# → 日期时间自动转为 ISO 格式字符串
# → 定义 response_model 时可以过滤敏感字段

2.4 自动文档的原理

FastAPI 在应用启动时,遍历所有注册的路由,根据你的类型注解自动生成 OpenAPI(原 Swagger)规范的 JSON 文件。然后基于这个 JSON 渲染出两个交互式文档:

  • Swagger UI/docs):可以在这个页面直接调试接口
  • ReDoc/redoc):更清晰的可读性文档

这意味着:你不需要单独维护一份文档。修改了参数类型,文档自动更新。代码即文档。


3. 核心体系:FastAPI 的请求生命周期

理解 FastAPI 的最好方式,是跟踪一个请求从进入到返回的全过程。下图展示了这个生命周期:

在这个生命周期中,核心知识点分布在五个层面,下面逐一拆解。

3.1 路由与参数体系

FastAPI 的路由定义非常直观:@app.get()@app.post() 等装饰器绑定 URL 路径和 HTTP 方法。参数从哪里来,由函数签名中的类型和默认值决定:

路径参数 :用 {} 包裹变量名,FastAPI 自动从 URL 中提取,按类型注解做转换。

python 复制代码
@app.get('/users/{user_id}')
async def get_user(user_id: int):  # 访问 /users/abc 自动返回 422
    return {'user_id': user_id}

查询参数:函数参数中不属于路径占位符的,自动识别为查询参数。

python 复制代码
@app.get('/items/')
async def list_items(
    skip: int = 0,       # 可选,默认 0
    limit: int = 10,     # 可选,默认 10
    category: str,        # 必选,没有默认值
):
    return {'skip': skip, 'limit': limit, 'category': category}

请求体参数:Pydantic 模型类型的参数,自动从 JSON 请求体中解析。

python 复制代码
@app.post('/books/')
async def create_book(book: BookIn):  # BookIn 继承自 BaseModel
    return {'id': 1, **book.model_dump()}

请求头与 Cookie :使用 Header()Cookie() 显式声明。

python 复制代码
@app.get('/secure')
async def secure_endpoint(
    token: str = Header(..., alias='Authorization'),
    session_id: str = Cookie(None),
):
    return {'valid': True}

表单与文件 :使用 Form()UploadFile

python 复制代码
from fastapi import Form, File, UploadFile

@app.post('/login')
async def login(
    username: str = Form(...),
    password: str = Form(...),
    avatar: UploadFile = File(None),
):
    return {'username': username}

3.2 参数校验体系

FastAPI 的参数校验分两层:

第一层:类型注解自带的校验

python 复制代码
item_id: int      # 自动校验是否为整数
price: float      # 自动校验是否为浮点数

第二层:Query() / Path() / Field() 提供的增强校验

python 复制代码
from typing import Annotated
from fastapi import Query, Path

# Annotated 写法(推荐,Python 3.9+)
@app.get('/items/')
async def read_items(
    q: Annotated[str | None, Query(
        min_length=3,
        max_length=50,
        pattern='^[a-zA-Z0-9]+$',
        description='搜索关键词',
    )] = None,
    page: Annotated[int, Query(ge=1)] = 1,
):
    pass

注意Annotated 写法的优势是类型信息完整,IDE 能正确提示。旧的 q: str = Query(default=None, min_length=3) 写法把默认值和校验规则混在一起,IDE 可能会误以为 q 总是字符串。

在 Pydantic 模型中,Field() 承担同样的职责:

python 复制代码
class Book(BaseModel):
    title: str = Field(..., min_length=2, max_length=100, description='书名')
    price: float = Field(..., gt=0, le=10000, description='价格')
    tags: list[str] = Field(default=[], max_length=5)

gt(大于)、ge(大于等于)、lt(小于)、le(小于等于)、min_lengthmax_lengthpattern(正则)------这些校验参数覆盖了 90% 的日常需求。如果还不够,可以用 AfterValidator 写自定义校验函数。

3.3 响应处理

FastAPI 的响应处理有三大机制:

响应模型(response_model) :这是 FastAPI 的杀手锏之一。通过声明 response_model,框架会:

  1. 自动过滤掉不在模型中的字段
  2. 自动做类型转换和校验
  3. 自动生成文档
python 复制代码
class UserIn(BaseModel):
    username: str
    password: str      # 输入时需要
    email: str

class UserOut(BaseModel):
    username: str
    email: str         # 响应时不暴露密码

@app.post('/users/', response_model=UserOut)
async def create_user(user: UserIn):
    return user  # password 会被自动过滤掉

响应状态码 :通过 status_code 指定,推荐使用 fastapi.status 中的常量。

python 复制代码
from fastapi import status

@app.post('/items/', status_code=status.HTTP_201_CREATED)
async def create_item():
    return {'message': 'created'}

响应类型家族:FastAPI 提供了多种响应类,覆盖不同场景:

响应类 Content-Type 适用场景
JSONResponse(默认) application/json 常规 JSON 数据
HTMLResponse text/html 返回 HTML 页面
PlainTextResponse text/plain 返回纯文本
RedirectResponse 3xx 状态码 URL 重定向
FileResponse 自动识别 文件下载
StreamingResponse 自定义 流式输出、大文件下载、SSE

流式响应(StreamingResponse) 值得单独拿出来讲,因为它在 LLM 应用、大文件下载场景下非常实用:

python 复制代码
from fastapi.responses import StreamingResponse

async def file_iterator(file_path: str, chunk_size: int = 8192):
    """逐块读取文件,避免内存溢出"""
    with open(file_path, 'rb') as f:
        while chunk := f.read(chunk_size):
            yield chunk

@app.get('/stream/file')
async def stream_large_file():
    return StreamingResponse(
        content=file_iterator('./large_file.zip'),
        media_type='application/octet-stream',
    )

核心思想是:不用一次性把整个文件读到内存,而是边读边发。文件大小和内存占用无关。

SSE(Server-Sent Events) 是另一种流式响应,用于服务端向客户端单向推送:

python 复制代码
@app.get('/stream/sse')
async def sse_stream():
    async def sse_generator():
        for i in range(10):
            yield f'data: 第 {i} 条消息\n\n'.encode('utf-8')

    return StreamingResponse(
        content=sse_generator(),
        media_type='text/event-stream',
        headers={
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
        }
    )

3.4 异常处理

FastAPI 的异常处理遵循"即抛即停 "原则:raise HTTPException 后,当前请求立即终止,返回指定的状态码和错误信息。

python 复制代码
from fastapi import HTTPException

@app.get('/items/{item_id}')
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(
            status_code=404,
            detail='Item not found',
            headers={'X-Error': 'not_found'},
        )
    return {'item': items[item_id]}

自定义异常处理器可以统一处理业务异常:

python 复制代码
from fastapi import Request
from fastapi.responses import JSONResponse

class BusinessError(Exception):
    def __init__(self, code: int, message: str):
        self.code = code
        self.message = message

@app.exception_handler(BusinessError)
async def business_error_handler(request: Request, exc: BusinessError):
    return JSONResponse(
        status_code=exc.code,
        content={'code': exc.code, 'message': exc.message},
    )

# 使用
raise BusinessError(400, '库存不足')

这样整个应用的错误响应格式就统一了。

3.5 依赖注入:FastAPI 的设计精髓

依赖注入是 FastAPI 与其他框架拉开差距的关键特性。一句话解释:

"你的函数需要什么,就声明什么,FastAPI 负责帮你取来。"

基础用法 :定义一个普通的函数作为依赖,通过 Depends() 注入到路径操作中。

python 复制代码
from typing import Annotated
from fastapi import Depends

# 定义依赖函数
async def common_params(skip: int = 0, limit: int = 10):
    return {'skip': skip, 'limit': limit}

# 注入到路径操作
@app.get('/items/')
async def read_items(params: Annotated[dict, Depends(common_params)]):
    return params

依赖链:依赖可以依赖其他依赖,形成链式调用。

python 复制代码
def get_query(q: str | None = None):
    return q

def get_query_or_empty(q: Annotated[str, Depends(get_query)]):
    return q or 'empty'

@app.get('/search/')
async def search(query: Annotated[str, Depends(get_query_or_empty)]):
    return {'query': query}

yield 依赖(资源管理) :当一个依赖需要管理资源(如数据库连接)时,用 yield 代替 returnyield 之前的代码在请求前执行,之后的代码在响应后执行。

python 复制代码
async def get_db():
    db = DatabaseSession()
    try:
        yield db  # 请求期间使用这个 db
    finally:
        await db.close()  # 请求结束后自动关闭

@app.get('/users/')
async def read_users(db: Annotated[DatabaseSession, Depends(get_db)]):
    return db.query(User).all()

依赖的作用范围

范围 写法 生效范围
全局 app = FastAPI(dependencies=[Depends(auth)]) 所有路由
模块级 APIRouter(dependencies=[Depends(auth)]) 该模块所有路由
路由级 @router.get('/', dependencies=[Depends(auth)]) 单个路由,不注入返回值
参数级 func(param = Depends(func)) 单个函数参数

依赖覆盖(测试用):测试时可以替换依赖,比如用 Mock 数据库替换真实数据库:

python 复制代码
app.dependency_overrides[get_db] = override_get_db
# 测试期间,所有用到 get_db 的地方都会使用 override_get_db

4. 避坑指南 / 最佳实践

4.1 推荐使用 Annotated 写法

新旧两种写法对比:

python 复制代码
# 旧写法:默认值和校验混在一起
q: str = Query(default=None, min_length=3, max_length=50)

# 新写法:类型、校验、默认值位置清晰
q: Annotated[str | None, Query(min_length=3, max_length=50)] = None

新写法有两大好处:

  • IDE 类型提示更准确(旧写法中 IDE 可能认为 q 总是 str,导致后续代码误报)
  • 参数结构更清晰:类型注解中的 Query() 只负责校验规则,最后的 = None 才是默认值

4.2 response_model 的安全问题

response_model 过滤敏感字段时要注意:FastAPI 是在数据返回前做过滤的 ,如果你在路径操作函数内部就把用户密码打印到了日志里,response_model 可帮不了你。

python 复制代码
# 错误做法:敏感字段输出了才过滤
class UserOut(BaseModel):
    username: str
    email: str

@app.post('/users/', response_model=UserOut)
async def create_user(user: UserIn):
    print(user.password)  # 密码已经在内存中了
    return user

最佳实践:在定义 Pydantic 模型时,输入模型和输出模型分开设计,输入模型包含所有字段(包括敏感信息),输出模型只包含需要暴露的字段。

4.3 Depends 的作用范围选择

遵循最小范围原则:能用参数级就不用全局。全局依赖会影响到所有路由,包括健康检查接口、静态文件等本不需要鉴权的路径。

python 复制代码
# 比较好的分层做法
# 1. 公开路由:不需要依赖
@app.get('/health')
async def health_check():
    return {'status': 'ok'}

# 2. 业务路由模块:统一加模块级鉴权
admin_router = APIRouter(
    prefix='/admin',
    dependencies=[Depends(verify_admin_token)],
)

4.4 用 APIRouter 组织项目

不要让 main.py 变成一个上千行的文件。按业务模块拆分:

复制代码
app/
├── main.py                 # 入口:初始化 App,挂载路由
├── api/
│   └── v1/
│       ├── api.py          # 汇总层
│       └── endpoints/
│           ├── books.py    # 图书模块
│           └── users.py    # 用户模块
├── schemas/                # Pydantic 模型
├── models/                 # SQLAlchemy 模型(后续数据库篇会讲)
└── core/                   # 配置、安全等

4.5 SSE vs WebSocket 选型

场景 选哪个 原因
推送通知、实时数据 SSE 单向足够,浏览器自动重连,实现简单
聊天、协作编辑 WebSocket 双向通信,低延迟
LLM 流式输出 SSE 服务端单向推送,客户端用 EventSource 接收
在线游戏 WebSocket 需要高频双向交互

4.6 官方文档


5. 总结

下表总结了 FastAPI 基础篇的核心概念和对应的 API:

你要做什么 用 FastAPI 的什么 一句话
定义路由 @app.get() / @app.post() 装饰器绑定 URL + HTTP 方法
路径参数 {id} + 类型注解 自动提取 URL 变量并校验类型
查询参数 Query() / 默认值 自动提取 ?key=value
请求体验证 Pydantic BaseModel 自动解析 JSON + 校验 + 文档
响应过滤 response_model 自动剔除敏感字段
流式输出 StreamingResponse 逐块返回,不占内存
异常处理 HTTPException 即抛即停,返回标准错误
依赖管理 Depends() 一次定义,随处注入
模块化路由 APIRouter 按业务拆分,include_router 聚合
跨域 CORSMiddleware add_middleware 一行配置

FastAPI 的核心设计哲学可以用一句话概括:让类型注解替你干活 。当你习惯用 AnnotatedBaseModelDepends 来表达意图时,你会发现写 API 不再是"参数搬运工",而是真正在写业务逻辑。


下篇预告:FastAPI 进阶篇------中间件机制、安全认证(OAuth2 + JWT)、后台任务、WebSocket 实时通信与项目工程化。这些内容会让你的 FastAPI 项目从"能用"变成"能上线"。