API 设计中 UUID 的使用指南
为什么在 API 中使用 UUID
在 REST API 设计中,UUID 作为资源标识符比自增整数 ID 有明显优势:安全性更高——不暴露数据库记录数量和顺序,防止用户通过 /users/1、/users/2 枚举资源;支持客户端生成 ID(幂等创建)——客户端可以在发送 POST 请求前生成 UUID,使创建操作幂等(重试不会创建重复记录);分布式友好——多个服务、多个节点可以独立生成 ID,无冲突;URL 语义更清晰——UUID 在 URL 中一目了然地标识一个特定资源,而不是一个可能变化的序号。
REST API 路径设计规范
# 推荐的 UUID 路径格式(小写,带连字符)
GET /api/v1/users/550e8400-e29b-41d4-a716-446655440000
PUT /api/v1/users/550e8400-e29b-41d4-a716-446655440000
DELETE /api/v1/users/550e8400-e29b-41d4-a716-446655440000
# 嵌套资源
GET /api/v1/users/550e8400-e29b-41d4-a716-446655440000/orders
GET /api/v1/orders/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
# 幂等创建(客户端提供 UUID)
PUT /api/v1/resources/client-generated-uuid
# 服务端如果已存在该 UUID,可以返回 200 或 409
# API 响应格式规范(统一使用字符串)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Example User",
"createdAt": "2025-01-01T00:00:00Z"
}
格式规范化建议
在 API 中关于 UUID 格式的建议:接受格式宽松(输入时同时接受大小写、带或不带花括号),输出格式严格(始终以小写无花括号格式返回);在路径参数中,对格式进行验证,格式不合法时返回 400 Bad Request 而不是 404 Not Found(告知调用方是格式问题而非"资源不存在");在 JSON body 中,UUID 以字符串形式传输(不要尝试用数字表示,会丢失精度);API 文档中明确说明使用 UUID v4,并提供格式示例。
错误处理最佳实践
# FastAPI 示例:UUID 参数验证和错误处理
from uuid import UUID
from fastapi import FastAPI, HTTPException, Path
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(ValueError)
async def uuid_exception_handler(request, exc):
return JSONResponse(
status_code=400,
content={
"error": "invalid_uuid",
"message": "The provided ID is not a valid UUID format",
"expected": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
)
@app.get("/api/users/{user_id}")
async def get_user(
user_id: UUID = Path(..., description="User UUID in RFC 4122 format")
):
user = db.find_user(user_id)
if not user:
raise HTTPException(
status_code=404,
detail={
"error": "not_found",
"message": f"User {user_id} not found"
}
)
return user
幂等性与重试安全
UUID 在 API 幂等性设计中非常有用。标准模式:客户端在首次请求前生成一个 UUID 作为 Idempotency-Key 请求头,服务端将此 UUID 与请求结果缓存(通常在 Redis 中保存 24 小时);如果相同的 Idempotency-Key 再次请求,直接返回缓存的响应,而不重复执行操作;这对于支付、订单创建等不能重复执行的操作特别重要。Stripe、Adyen 等支付平台都使用这种模式,允许客户端安全重试失败的请求而不产生副作用。
# 幂等 API 调用示例
import uuid
import httpx
def create_order_idempotent(order_data, max_retries=3):
idempotency_key = str(uuid.uuid4()) # 本次调用全局唯一
for attempt in range(max_retries):
try:
response = httpx.post(
'/api/orders',
json=order_data,
headers={'Idempotency-Key': idempotency_key}
)
if response.status_code in (200, 201):
return response.json()
elif response.status_code == 409:
return response.json() # 幂等:返回已存在的结果
except httpx.NetworkError:
if attempt == max_retries - 1:
raise
# 使用相同的 idempotency_key 重试
GraphQL 中的 UUID 使用
# GraphQL schema 示例
scalar UUID
type User {
id: UUID! # 非空 UUID 类型
name: String!
email: String!
posts: [Post!]!
}
type Query {
user(id: UUID!): User
users: [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: UUID!, input: UpdateUserInput!): User!
deleteUser(id: UUID!): Boolean!
}
# 查询示例
query {
user(id: "550e8400-e29b-41d4-a716-446655440000") {
id
name
email
}
}
立即免费使用相关工具
免费使用 →