3.3. RESTful API 设计

良好的 API 设计是构建可维护、易用系统的关键。

3.3.1. REST 原则

3.3.1.1. 资源命名

# ✅ 好的 URL 设计
GET    /users              # 获取用户列表
GET    /users/123          # 获取特定用户
POST   /users              # 创建用户
PUT    /users/123          # 更新用户(完整替换)
PATCH  /users/123          # 部分更新用户
DELETE /users/123          # 删除用户

# 嵌套资源
GET    /users/123/orders           # 用户的订单
GET    /users/123/orders/456       # 特定订单
POST   /users/123/orders           # 创建订单

# ❌ 不好的设计
GET    /getUser/123                # 动词放 URL
GET    /users/123/getOrders        # 冗余
POST   /users/createUser           # 冗余

3.3.1.2. HTTP 方法语义

方法

用途

幂等性

安全性

GET

获取资源

POST

创建资源

PUT

完整更新

PATCH

部分更新

DELETE

删除资源

3.3.1.3. 状态码

from fastapi import HTTPException, status

# 成功响应
# 200 OK - 通用成功
# 201 Created - 创建成功
# 204 No Content - 删除成功(无返回内容)

@app.post("/users", status_code=status.HTTP_201_CREATED)
async def create_user(user: UserCreate):
    return new_user

@app.delete("/users/{id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(id: int):
    pass

# 客户端错误
# 400 Bad Request - 请求格式错误
# 401 Unauthorized - 未认证
# 403 Forbidden - 无权限
# 404 Not Found - 资源不存在
# 409 Conflict - 冲突(如重复创建)
# 422 Unprocessable Entity - 验证失败

# 服务端错误
# 500 Internal Server Error - 服务器错误
# 503 Service Unavailable - 服务不可用

3.3.2. 请求与响应设计

3.3.2.1. 统一响应格式

from pydantic import BaseModel
from typing import Generic, TypeVar, Optional, List

T = TypeVar('T')

class Response(BaseModel, Generic[T]):
    """统一响应格式"""
    success: bool
    data: Optional[T] = None
    error: Optional[str] = None
    
class PagedResponse(BaseModel, Generic[T]):
    """分页响应"""
    items: List[T]
    total: int
    page: int
    page_size: int
    pages: int

# 使用示例
@app.get("/users/{id}", response_model=Response[UserResponse])
async def get_user(id: int):
    user = await get_user_by_id(id)
    return Response(success=True, data=user)

@app.get("/users", response_model=PagedResponse[UserResponse])
async def list_users(page: int = 1, page_size: int = 20):
    users, total = await get_users_paginated(page, page_size)
    return PagedResponse(
        items=users,
        total=total,
        page=page,
        page_size=page_size,
        pages=(total + page_size - 1) // page_size
    )

3.3.2.2. 错误响应

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import List, Optional

class ErrorDetail(BaseModel):
    field: Optional[str] = None
    message: str

class ErrorResponse(BaseModel):
    error: str
    code: str
    details: Optional[List[ErrorDetail]] = None

# 自定义异常
class APIError(Exception):
    def __init__(self, message: str, code: str, status_code: int = 400):
        self.message = message
        self.code = code
        self.status_code = status_code

# 异常处理器
@app.exception_handler(APIError)
async def api_error_handler(request: Request, exc: APIError):
    return JSONResponse(
        status_code=exc.status_code,
        content=ErrorResponse(
            error=exc.message,
            code=exc.code
        ).dict()
    )

# Pydantic 验证错误处理
from fastapi.exceptions import RequestValidationError

@app.exception_handler(RequestValidationError)
async def validation_error_handler(request: Request, exc: RequestValidationError):
    details = [
        ErrorDetail(field=".".join(map(str, err["loc"])), message=err["msg"])
        for err in exc.errors()
    ]
    return JSONResponse(
        status_code=422,
        content=ErrorResponse(
            error="Validation failed",
            code="VALIDATION_ERROR",
            details=details
        ).dict()
    )

3.3.3. 版本控制

3.3.3.1. URL 版本

# 推荐:URL 路径版本
from fastapi import APIRouter

v1_router = APIRouter(prefix="/api/v1")
v2_router = APIRouter(prefix="/api/v2")

@v1_router.get("/users")
async def get_users_v1():
    return {"version": "v1"}

@v2_router.get("/users")
async def get_users_v2():
    return {"version": "v2", "new_field": "value"}

app.include_router(v1_router)
app.include_router(v2_router)

3.3.3.2. 请求头版本

from fastapi import Header, Depends

async def get_api_version(
    api_version: str = Header("v1", alias="X-API-Version")
):
    return api_version

@app.get("/users")
async def get_users(version: str = Depends(get_api_version)):
    if version == "v2":
        return {"new_format": True}
    return {"old_format": True}

3.3.4. 过滤、排序、分页

from fastapi import Query
from typing import List, Optional
from enum import Enum

class SortOrder(str, Enum):
    asc = "asc"
    desc = "desc"

@app.get("/users")
async def list_users(
    # 分页
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    
    # 过滤
    status: Optional[str] = Query(None),
    created_after: Optional[datetime] = Query(None),
    search: Optional[str] = Query(None),
    
    # 排序
    sort_by: str = Query("created_at"),
    sort_order: SortOrder = Query(SortOrder.desc),
):
    query = User.query
    
    # 应用过滤
    if status:
        query = query.filter(User.status == status)
    if created_after:
        query = query.filter(User.created_at >= created_after)
    if search:
        query = query.filter(
            User.name.ilike(f"%{search}%") |
            User.email.ilike(f"%{search}%")
        )
    
    # 应用排序
    order_col = getattr(User, sort_by)
    if sort_order == SortOrder.desc:
        order_col = order_col.desc()
    query = query.order_by(order_col)
    
    # 分页
    total = query.count()
    users = query.offset((page - 1) * page_size).limit(page_size).all()
    
    return {
        "items": users,
        "total": total,
        "page": page,
        "page_size": page_size
    }

3.3.5. HATEOAS(超媒体)

from pydantic import BaseModel
from typing import Dict, List

class Link(BaseModel):
    href: str
    rel: str
    method: str = "GET"

class UserWithLinks(BaseModel):
    id: int
    name: str
    email: str
    _links: Dict[str, Link]

@app.get("/users/{id}")
async def get_user(id: int, request: Request):
    user = await get_user_by_id(id)
    
    base_url = str(request.base_url)
    
    return UserWithLinks(
        **user.dict(),
        _links={
            "self": Link(href=f"{base_url}users/{id}", rel="self"),
            "orders": Link(href=f"{base_url}users/{id}/orders", rel="orders"),
            "update": Link(href=f"{base_url}users/{id}", rel="update", method="PUT"),
            "delete": Link(href=f"{base_url}users/{id}", rel="delete", method="DELETE"),
        }
    )

3.3.6. API 文档

3.3.6.1. OpenAPI 增强

from fastapi import FastAPI

app = FastAPI(
    title="My API",
    description="""
    ## 功能特性
    
    * **用户管理** - 创建、查询、更新、删除用户
    * **订单处理** - 订单相关操作
    
    ## 认证
    
    使用 Bearer Token 认证,在请求头中添加:
    ```
    Authorization: Bearer <token>
    ```
    """,
    version="1.0.0",
    contact={
        "name": "API Support",
        "email": "support@example.com"
    },
    license_info={
        "name": "MIT"
    }
)

# 为端点添加详细文档
@app.get(
    "/users/{user_id}",
    summary="获取用户详情",
    description="根据用户 ID 获取用户的详细信息",
    response_description="用户详细信息",
    responses={
        200: {"description": "成功获取用户"},
        404: {"description": "用户不存在"},
    }
)
async def get_user(user_id: int):
    """
    获取指定用户的详细信息:
    
    - **user_id**: 用户的唯一标识符
    
    返回用户的完整信息,包括:
    - 基本信息(姓名、邮箱等)
    - 创建时间
    - 最后更新时间
    """
    pass

3.3.7. 最佳实践

设计原则
  1. 一致性:URL、响应格式、错误处理保持一致

  2. 向后兼容:使用版本控制,避免破坏性变更

  3. 自描述:良好的命名和文档

  4. 无状态:每个请求包含所需的全部信息

安全考虑
  1. 始终使用 HTTPS

  2. 验证所有输入

  3. 限制响应数据(不返回敏感字段)

  4. 实施速率限制

  5. 使用适当的认证机制

性能优化
  1. 支持条件请求(ETag, Last-Modified)

  2. 实现分页

  3. 支持字段选择(稀疏字段集)

  4. 使用缓存

  5. 压缩响应(gzip)