FastAPI请求与响应:掌握API对话的艺术
在RESTful API的世界里,请求与响应就像是客户端与服务端之间的对话。掌握这门艺术,你就能构建出优雅、健壮的API接口。
1. 路径参数:定义API的访问地址
路径参数是RESTful API中最基础的部分,它们定义了资源的访问路径。FastAPI让路径参数的处理变得异常简单。
基础路径参数
python
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
"""基本的路径参数,FastAPI会自动进行类型转换"""
return {"item_id": item_id}
# 访问示例:
# GET /items/42 → {"item_id": 42}
# GET /items/not-a-number → 自动返回422错误路径参数的高级用法
python
from enum import Enum
from datetime import date
from uuid import UUID
from typing import Optional
# 枚举类型的路径参数
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
"""使用枚举限制参数值"""
if model_name == ModelName.alexnet:
return {"model_name": model_name, "message": "Deep Learning FTW!"}
if model_name.value == "lenet":
return {"model_name": model_name, "message": "LeCNN all the images"}
return {"model_name": model_name, "message": "Have some residuals"}
# 文件路径参数
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
"""接收文件路径参数"""
return {"file_path": file_path}
# 日期参数
@app.get("/events/{event_date}")
async def get_events(event_date: date):
"""日期类型的路径参数"""
return {"date": event_date, "events": ["会议", "发布会"]}
# UUID参数
@app.get("/users/{user_id}")
async def get_user(user_id: UUID):
"""UUID类型的路径参数"""
return {"user_id": user_id, "name": "张三"}路径参数验证
python
from fastapi import Path
@app.get("/items/{item_id}/detail")
async def read_item_detail(
item_id: int = Path(
...,
title="商品ID",
description="要查询的商品唯一标识符",
gt=0, # 大于0
le=1000, # 小于等于1000
example=123
)
):
"""带验证的路径参数"""
return {"item_id": item_id, "detail": "商品详细信息"}
# 正则表达式验证
@app.get("/users/{username}")
async def get_user_by_name(
username: str = Path(
...,
regex="^[a-zA-Z0-9_]{3,20}$", # 用户名正则验证
description="用户名,3-20位字母数字下划线"
)
):
return {"username": username}2. 查询参数:灵活的请求过滤器
查询参数是API中最灵活的过滤工具,用于可选的、非必需的参数传递。
基础查询参数
python
from typing import Optional, List
@app.get("/items/")
async def read_items(
skip: int = 0, # 默认值为0
limit: int = 10, # 默认值为10
q: Optional[str] = None # 可选参数
):
"""基础查询参数示例"""
items = [
{"id": 1, "name": "商品1"},
{"id": 2, "name": "商品2"},
{"id": 3, "name": "商品3"}
]
result = items[skip:skip+limit]
if q:
result = [item for item in result if q.lower() in item["name"].lower()]
return {
"skip": skip,
"limit": limit,
"q": q,
"items": result
}
# 访问示例:
# GET /items/ → 返回前10个商品
# GET /items/?skip=5&limit=3 → 跳过5个,返回3个
# GET /items/?q=商品 → 搜索包含"商品"的商品查询参数验证
python
from fastapi import Query
@app.get("/search/")
async def search_items(
q: Optional[str] = Query(
None,
min_length=3, # 最小长度
max_length=50, # 最大长度
title="搜索关键词",
description="搜索商品的名称或描述",
example="笔记本电脑"
),
category: Optional[str] = Query(
None,
regex="^(电子产品|书籍|服装)$", # 枚举值验证
description="商品分类"
),
price_min: Optional[float] = Query(
None,
ge=0, # 大于等于0
description="最低价格"
),
price_max: Optional[float] = Query(
None,
gt=0, # 大于0
description="最高价格"
),
tags: List[str] = Query([], description="标签筛选")
):
"""带验证的查询参数"""
# 实际应用中会查询数据库
return {
"query": {
"q": q,
"category": category,
"price_min": price_min,
"price_max": price_max,
"tags": tags
},
"results": [] # 搜索结果
}
# 必需查询参数
@app.get("/required-search/")
async def required_search(
keyword: str = Query(..., min_length=1, description="搜索关键词")
):
"""必需的查询参数"""
return {"keyword": keyword, "results": []}查询参数的更多特性
python
from typing import Union
# 多种类型参数
@app.get("/mixed-params/")
async def mixed_params(
param: Union[int, str, None] = None, # 可以是int或str
sort_by: str = Query("id", description="排序字段"),
sort_order: str = Query("asc", regex="^(asc|desc)$")
):
"""混合类型的查询参数"""
return {
"param": param,
"sort_by": sort_by,
"sort_order": sort_order
}
# 别名参数
@app.get("/alias/")
async def alias_params(
item_query: str = Query(None, alias="item-query"), # API中使用item-query
user_name: str = Query(None, alias="user-name")
):
"""使用别名的查询参数"""
return {
"item_query": item_query,
"user_name": user_name
}
# 弃用参数
@app.get("/deprecated/")
async def deprecated_params(
old_param: str = Query(
None,
deprecated=True, # 标记为已弃用
description="已弃用,请使用new_param"
),
new_param: str = Query(None, description="新参数")
):
"""包含已弃用参数的接口"""
return {"new_param": new_param}3. 请求体:处理复杂数据输入
请求体用于接收客户端发送的复杂数据,通常是JSON格式。
基础请求体
python
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
@app.post("/items/")
async def create_item(item: Item):
"""创建商品 - 基础请求体"""
# 计算含税价格
price_with_tax = item.price
if item.tax:
price_with_tax = item.price * (1 + item.tax)
return {
**item.dict(),
"price_with_tax": price_with_tax,
"created_at": datetime.now()
}
# 使用示例:
# POST /items/
# {
# "name": "笔记本电脑",
# "description": "高性能游戏本",
# "price": 8999.99,
# "tax": 0.13
# }路径参数 + 查询参数 + 请求体
python
@app.put("/items/{item_id}")
async def update_item(
item_id: int, # 路径参数
item: Item, # 请求体
q: Optional[str] = None # 查询参数
):
"""综合使用所有参数类型"""
result = {"item_id": item_id, **item.dict()}
if q:
result.update({"q": q})
return result多个请求体参数
python
class User(BaseModel):
username: str
email: EmailStr
@app.put("/multi-body/")
async def update_multiple_items(
item: Item,
user: User,
importance: int = Body(..., gt=0) # 单独的Body参数
):
"""多个请求体参数"""
return {
"item": item,
"user": user,
"importance": importance
}4. Pydantic模型:数据验证的神器
Pydantic是FastAPI的数据验证核心,它基于Python类型提示提供了强大的数据验证功能。
基础模型定义
python
from pydantic import BaseModel, Field, validator
from typing import List, Optional
from datetime import datetime
class Product(BaseModel):
id: int
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
price: float = Field(..., gt=0, description="价格必须大于0")
in_stock: bool = True
tags: List[str] = Field(default_factory=list)
created_at: datetime = Field(default_factory=datetime.now)
# 自定义验证器
@validator('name')
def name_must_contain_space(cls, v):
if ' ' not in v:
raise ValueError('名称必须包含空格')
return v.title() # 自动转换为标题格式
@validator('price')
def price_must_be_reasonable(cls, v):
if v > 1000000:
raise ValueError('价格过高,请检查')
return round(v, 2) # 保留两位小数复杂嵌套模型
python
class Address(BaseModel):
street: str
city: str
postal_code: str
country: str = "中国"
class OrderItem(BaseModel):
product_id: int
quantity: int = Field(..., gt=0, le=100)
class Order(BaseModel):
order_id: str
customer_name: str
shipping_address: Address # 嵌套模型
items: List[OrderItem] # 列表嵌套
total_amount: float
notes: Optional[str] = None
# 跨字段验证
@validator('total_amount')
def validate_total_amount(cls, v, values):
if 'items' in values:
# 这里可以计算总价并与v比较
pass
return v
# 使用示例
order_data = {
"order_id": "ORD123456",
"customer_name": "张三",
"shipping_address": {
"street": "人民路123号",
"city": "北京",
"postal_code": "100000"
},
"items": [
{"product_id": 1, "quantity": 2},
{"product_id": 2, "quantity": 1}
],
"total_amount": 299.98
}模型继承与复用
python
# 基础模型
class BaseUser(BaseModel):
email: EmailStr
full_name: Optional[str] = None
# 创建模型
class UserCreate(BaseUser):
password: str = Field(..., min_length=8)
# 更新模型(所有字段可选)
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
full_name: Optional[str] = None
password: Optional[str] = Field(None, min_length=8)
# 响应模型(排除敏感信息)
class UserResponse(BaseUser):
id: int
is_active: bool
created_at: datetime
class Config:
orm_mode = True # 支持ORM对象转换5. 响应模型:控制API输出格式
响应模型让你能够精确控制API返回的数据结构和格式。
基础响应模型
python
@app.post("/users/", response_model=UserResponse)
async def create_user(user: UserCreate):
"""创建用户,返回响应模型"""
# 实际应用中这里会保存到数据库
db_user = {
"id": 1,
"email": user.email,
"full_name": user.full_name,
"is_active": True,
"created_at": datetime.now()
}
return db_user # 自动转换为UserResponse格式排除敏感字段
python
class PrivateUser(BaseUser):
id: int
hashed_password: str
class PublicUser(BaseUser):
id: int
class Config:
orm_mode = True
@app.get("/users/{user_id}", response_model=PublicUser)
async def get_user(user_id: int):
"""获取用户信息,排除敏感字段"""
# 模拟数据库查询
private_user = {
"id": user_id,
"email": "user@example.com",
"full_name": "测试用户",
"hashed_password": "secret_hash" # 这个字段不会在响应中出现
}
return private_user # 只有PublicUser中定义的字段会被返回响应状态码与自定义响应
python
from fastapi import status
from fastapi.responses import JSONResponse
@app.post(
"/create-item/",
response_model=Item,
status_code=status.HTTP_201_CREATED,
responses={
201: {
"description": "成功创建",
"content": {
"application/json": {
"example": {
"name": "示例商品",
"price": 99.99,
"tax": 0.13
}
}
}
},
422: {
"description": "验证错误",
"content": {
"application/json": {
"example": {
"detail": [
{
"loc": ["body", "price"],
"msg": "价格必须大于0",
"type": "value_error"
}
]
}
}
}
}
}
)
async def create_item_with_custom_response(item: Item):
"""自定义响应"""
# 检查商品是否存在
if item.name == "已存在商品":
return JSONResponse(
status_code=status.HTTP_409_CONFLICT,
content={"detail": "商品已存在"}
)
return item6. 状态码与错误处理:优雅的异常对话
优雅的错误处理是专业API的重要标志。
自定义异常处理器
python
from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
# 自定义异常
class ItemNotFoundError(Exception):
def __init__(self, item_id: int):
self.item_id = item_id
# 异常处理器
@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request: Request, exc: ItemNotFoundError):
return JSONResponse(
status_code=404,
content={
"error": "Item Not Found",
"message": f"找不到ID为 {exc.item_id} 的商品",
"request_id": request.headers.get("X-Request-ID", "unknown")
}
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""自定义验证错误响应格式"""
return JSONResponse(
status_code=422,
content={
"success": False,
"error": "Validation Error",
"details": exc.errors(),
"timestamp": datetime.now().isoformat()
}
)使用HTTPException
python
from fastapi import HTTPException
items_db = {1: {"name": "商品1", "price": 100}}
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id not in items_db:
raise HTTPException(
status_code=404,
detail="商品不存在",
headers={"X-Error": "Item not found"}
)
if item_id == 0:
# 自定义状态码
raise HTTPException(
status_code=418,
detail="我不能处理这个请求",
headers={"X-Error": "拒绝处理"}
)
return items_db[item_id]全局错误处理中间件
python
from fastapi.middleware.cors import CORSMiddleware
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.middleware("http")
async def catch_exceptions_middleware(request: Request, call_next):
try:
response = await call_next(request)
return response
except Exception as exc:
# 记录异常日志
logger.error(f"未处理的异常: {exc}", exc_info=True)
# 返回友好的错误信息
return JSONResponse(
status_code=500,
content={
"success": False,
"error": "Internal Server Error",
"message": "服务器内部错误,请稍后重试",
"request_id": request.headers.get("X-Request-ID")
}
)7. 文件上传与下载:处理二进制数据
FastAPI提供了强大的文件上传和下载功能。
单文件上传
python
from fastapi import File, UploadFile
import shutil
from pathlib import Path
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
@app.post("/upload/")
async def upload_file(
file: UploadFile = File(..., description="上传的文件")
):
"""单文件上传"""
# 验证文件类型
allowed_types = ["image/jpeg", "image/png", "application/pdf"]
if file.content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"不支持的文件类型: {file.content_type}"
)
# 验证文件大小(最大10MB)
MAX_SIZE = 10 * 1024 * 1024
contents = await file.read()
if len(contents) > MAX_SIZE:
raise HTTPException(
status_code=400,
detail="文件大小超过10MB限制"
)
# 保存文件
file_path = UPLOAD_DIR / file.filename
with open(file_path, "wb") as buffer:
buffer.write(contents)
return {
"filename": file.filename,
"content_type": file.content_type,
"size": len(contents),
"path": str(file_path)
}多文件上传
python
@app.post("/multi-upload/")
async def upload_multiple_files(
files: List[UploadFile] = File(..., description="多个文件")
):
"""多文件上传"""
results = []
for file in files:
# 为每个文件生成唯一文件名
import uuid
file_extension = Path(file.filename).suffix
unique_filename = f"{uuid.uuid4()}{file_extension}"
file_path = UPLOAD_DIR / unique_filename
# 异步保存文件
async with aiofiles.open(file_path, 'wb') as out_file:
content = await file.read()
await out_file.write(content)
results.append({
"original_filename": file.filename,
"saved_filename": unique_filename,
"size": len(content)
})
return {
"total_files": len(files),
"files": results
}文件下载
python
from fastapi.responses import FileResponse, StreamingResponse
import aiofiles
@app.get("/download/{filename}")
async def download_file(filename: str):
"""文件下载"""
file_path = UPLOAD_DIR / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail="文件不存在")
# 使用FileResponse自动处理文件下载
return FileResponse(
path=file_path,
filename=filename,
media_type='application/octet-stream'
)
@app.get("/stream-download/{filename}")
async def stream_download(filename: str):
"""流式下载大文件"""
file_path = UPLOAD_DIR / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail="文件不存在")
async def file_streamer():
"""异步文件流生成器"""
async with aiofiles.open(file_path, 'rb') as file:
chunk_size = 1024 * 1024 # 1MB chunks
while chunk := await file.read(chunk_size):
yield chunk
# 获取文件大小
file_size = file_path.stat().st_size
# 设置响应头
headers = {
"Content-Disposition": f"attachment; filename={filename}",
"Content-Length": str(file_size)
}
return StreamingResponse(
file_streamer(),
media_type="application/octet-stream",
headers=headers
)表单与文件混合上传
python
from fastapi import Form
@app.post("/upload-with-data/")
async def upload_with_metadata(
title: str = Form(...),
description: str = Form(None),
tags: List[str] = Form([]),
file: UploadFile = File(...)
):
"""表单数据和文件混合上传"""
# 保存文件
file_path = UPLOAD_DIR / file.filename
contents = await file.read()
with open(file_path, "wb") as buffer:
buffer.write(contents)
# 保存元数据到数据库(这里简化为返回)
metadata = {
"title": title,
"description": description,
"tags": tags,
"filename": file.filename,
"size": len(contents)
}
return metadata最佳实践总结
- 路径参数:用于标识资源,应该简洁明了
- 查询参数:用于过滤、排序、分页等操作
- 请求体:使用Pydantic模型确保数据验证
- 响应模型:明确API输出,保护敏感数据
- 错误处理:提供清晰、友好的错误信息
- 文件处理:注意安全性和性能考虑
通过掌握这些请求与响应的处理技巧,你就能构建出既强大又易用的API接口。记住,好的API设计就像好的对话——清晰、一致、友好。
思考题:在你的项目中,哪些API设计可以应用今天学到的技巧进行优化?欢迎在评论区分享你的实践!