摘要: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是给人用的,设计时多想想调用方的体验。