RESTful API设计最佳实践:从URL规范到错误处理

3 阅读5分钟

摘要:API是前后端协作的契约。设计得好,前端开发顺畅、文档自解释;设计得差,联调痛苦、维护噩梦。本文总结RESTful API设计的核心原则和实战规范。

URL设计

核心原则

# ✅ 好的URL:名词复数,层级清晰
GET    /api/v1/users              # 用户列表
GET    /api/v1/users/123          # 单个用户
POST   /api/v1/users              # 创建用户
PUT    /api/v1/users/123          # 更新用户(全量)
PATCH  /api/v1/users/123          # 更新用户(部分)
DELETE /api/v1/users/123          # 删除用户

# ✅ 嵌套资源
GET    /api/v1/users/123/posts         # 用户的文章列表
GET    /api/v1/users/123/posts/456     # 用户的某篇文章
POST   /api/v1/users/123/posts         # 为用户创建文章

# ❌ 坏的URL
GET    /api/getUser?id=123        # 动词放URL里
POST   /api/deleteUser            # 用POST做删除
GET    /api/v1/user               # 单数
GET    /api/v1/getUserPosts       # 驼峰命名

命名规范

# URL用小写 + 连字符
/api/v1/user-profiles          # ✅
/api/v1/userProfiles           # ❌ 驼峰
/api/v1/user_profiles          # ❌ 下划线

# JSON字段用驼峰
{"userId": 123, "userName": "张三"}    # ✅
{"user_id": 123, "user_name": "张三"}  # ❌(Python风格,但API推荐驼峰)

版本控制

# 方案1:URL路径(推荐,最直观)
/api/v1/users
/api/v2/users

# 方案2:请求头
Accept: application/vnd.myapp.v1+json

# 方案3:查询参数
/api/users?version=1

请求设计

查询参数

# 分页
GET /api/v1/users?page=1&pageSize=20

# 排序
GET /api/v1/users?sort=createdAt&order=desc
GET /api/v1/users?sort=-createdAt          # 简写:-表示降序

# 过滤
GET /api/v1/users?status=active&role=admin
GET /api/v1/users?age[gte]=18&age[lte]=30  # 范围查询

# 字段选择(减少传输量)
GET /api/v1/users?fields=id,name,email

# 搜索
GET /api/v1/users?q=张三

# 组合
GET /api/v1/users?status=active&sort=-createdAt&page=1&pageSize=20&fields=id,name

请求体

// POST /api/v1/users
{
    "name": "张三",
    "email": "zhang@test.com",
    "age": 25,
    "roles": ["user", "editor"]
}

// PATCH /api/v1/users/123(只传需要更新的字段)
{
    "age": 26
}

响应设计

统一响应格式

// 成功响应
{
    "code": 0,
    "message": "success",
    "data": {
        "id": 123,
        "name": "张三",
        "email": "zhang@test.com"
    }
}

// 列表响应(带分页)
{
    "code": 0,
    "message": "success",
    "data": {
        "items": [
            {"id": 1, "name": "张三"},
            {"id": 2, "name": "李四"}
        ],
        "pagination": {
            "page": 1,
            "pageSize": 20,
            "total": 156,
            "totalPages": 8
        }
    }
}

// 错误响应
{
    "code": 40001,
    "message": "邮箱格式不正确",
    "errors": [
        {
            "field": "email",
            "message": "请输入有效的邮箱地址"
        }
    ]
}

HTTP状态码

# 成功
200  # OK - 通用成功
201  # Created - 创建成功(POST)
204  # No Content - 删除成功(DELETE)

# 客户端错误
400  # Bad Request - 参数错误
401  # Unauthorized - 未认证(没登录)
403  # Forbidden - 无权限(登录了但没权限)
404  # Not Found - 资源不存在
409  # Conflict - 冲突(如邮箱已注册)
422  # Unprocessable Entity - 参数验证失败
429  # Too Many Requests - 限流

# 服务端错误
500  # Internal Server Error - 服务器内部错误
502  # Bad Gateway - 网关错误
503  # Service Unavailable - 服务不可用

FastAPI实现示例

from fastapi import FastAPI, HTTPException, Query, Path
from pydantic import BaseModel, EmailStr, Field
from typing import Generic, TypeVar
from datetime import datetime

app = FastAPI(title="用户管理API", version="1.0.0")

# ===== 数据模型 =====

class UserCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=50, examples=["张三"])
    email: EmailStr
    age: int = Field(..., ge=0, le=150)

class UserUpdate(BaseModel):
    name: str | None = None
    email: EmailStr | None = None
    age: int | None = Field(None, ge=0, le=150)

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    age: int
    createdAt: datetime

T = TypeVar('T')

class ApiResponse(BaseModel, Generic[T]):
    code: int = 0
    message: str = "success"
    data: T | None = None

class PaginatedData(BaseModel, Generic[T]):
    items: list[T]
    pagination: dict

# ===== 接口 =====

@app.get("/api/v1/users", response_model=ApiResponse[PaginatedData[UserResponse]])
async def list_users(
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100, alias="pageSize"),
    status: str | None = Query(None),
    sort: str = Query("-createdAt"),
    q: str | None = Query(None, description="搜索关键词"),
):
    """获取用户列表"""
    # 实际查询逻辑...
    return ApiResponse(data=PaginatedData(
        items=[],
        pagination={"page": page, "pageSize": page_size, "total": 0, "totalPages": 0}
    ))

@app.get("/api/v1/users/{user_id}", response_model=ApiResponse[UserResponse])
async def get_user(user_id: int = Path(..., ge=1)):
    """获取单个用户"""
    user = await find_user(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="用户不存在")
    return ApiResponse(data=user)

@app.post("/api/v1/users", response_model=ApiResponse[UserResponse], status_code=201)
async def create_user(body: UserCreate):
    """创建用户"""
    existing = await find_user_by_email(body.email)
    if existing:
        raise HTTPException(status_code=409, detail="邮箱已注册")
    user = await save_user(body)
    return ApiResponse(data=user)

@app.patch("/api/v1/users/{user_id}", response_model=ApiResponse[UserResponse])
async def update_user(user_id: int, body: UserUpdate):
    """更新用户(部分更新)"""
    update_data = body.model_dump(exclude_unset=True)  # 只取传了的字段
    if not update_data:
        raise HTTPException(status_code=400, detail="没有需要更新的字段")
    user = await patch_user(user_id, update_data)
    return ApiResponse(data=user)

@app.delete("/api/v1/users/{user_id}", status_code=204)
async def delete_user(user_id: int):
    """删除用户"""
    await remove_user(user_id)

认证方案

JWT Token

from fastapi import Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt

security = HTTPBearer()

async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security)
) -> dict:
    try:
        payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=["HS256"])
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "Token已过期")
    except jwt.InvalidTokenError:
        raise HTTPException(401, "无效的Token")

# 需要认证的接口
@app.get("/api/v1/me")
async def get_me(user: dict = Depends(get_current_user)):
    return ApiResponse(data=user)

API请求流程

客户端                          服务端
  |                               |
  |  POST /api/v1/auth/login      |
  |  {"email":"..","password":".."}|
  |------------------------------>|
  |                               |
  |  200 {"token": "eyJ..."}      |
  |<------------------------------|
  |                               |
  |  GET /api/v1/users            |
  |  Authorization: Bearer eyJ... |
  |------------------------------>|
  |                               |
  |  200 {"data": [...]}          |
  |<------------------------------|

限流设计

from fastapi import Request
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.get("/api/v1/search")
@limiter.limit("30/minute")
async def search(request: Request, q: str):
    ...

响应头告知限流状态:

X-RateLimit-Limit: 30
X-RateLimit-Remaining: 25
X-RateLimit-Reset: 1703275200

API文档

FastAPI自动生成OpenAPI文档:

app = FastAPI(
    title="用户管理系统",
    description="RESTful API文档",
    version="1.0.0",
    docs_url="/docs",        # Swagger UI
    redoc_url="/redoc",      # ReDoc
)

访问 http://localhost:8000/docs 即可看到交互式文档。

总结

好的API设计原则:

  • URL用名词复数,HTTP方法表达动作
  • 统一响应格式,前端不用猜
  • 状态码要准确,别什么都返回200
  • 分页、排序、过滤用查询参数
  • 错误信息要有用,别只返回"参数错误"
  • 版本控制从第一天就做

API是给人用的,设计时多想想调用方的体验。