Help us improve
Share bugs, ideas, or general feedback.
REST API design patterns covering resource naming, status codes, pagination, filtering, error responses, versioning, and rate limiting for production APIs.
npx claudepluginhub aaione/everything-claude-code-zhHow this skill is triggered — by the user, by Claude, or both
Slash command
/everything-claude-code:api-designThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
用于设计一致、对开发人员友好的 REST API 的约定和最佳实践。
Guides REST API design patterns for production-grade endpoints including resource naming, status codes, pagination, filtering, error responses, versioning, and rate limiting. Use when designing new APIs or reviewing contracts.
Establishes REST API design patterns for resource naming, HTTP methods and status codes, pagination, filtering, error responses, versioning, and rate limiting for production APIs.
Guides REST API design with standards for resource naming, versioning, and RFC 7807 error responses. Use when designing endpoints, pagination, or API structure.
Share bugs, ideas, or general feedback.
用于设计一致、对开发人员友好的 REST API 的约定和最佳实践。
# 资源是名词、复数、小写、kebab-case
GET /api/v1/users
GET /api/v1/users/:id
POST /api/v1/users
PUT /api/v1/users/:id
PATCH /api/v1/users/:id
DELETE /api/v1/users/:id
# 用于关系的子资源
GET /api/v1/users/:id/orders
POST /api/v1/users/:id/orders
# 不映射到 CRUD 的操作(谨慎使用动词)
POST /api/v1/orders/:id/cancel
POST /api/v1/auth/login
POST /api/v1/auth/refresh
# 好
/api/v1/team-members # 多词资源使用 kebab-case
/api/v1/orders?status=active # 查询参数用于过滤
/api/v1/users/123/orders # 嵌套资源表示所有权
# 坏
/api/v1/getUsers # URL 中的动词
/api/v1/user # 单数(使用复数)
/api/v1/team_members # URL 中的 snake_case
/api/v1/users/123/getOrders # 嵌套资源中的动词
| 方法 | 幂等 | 安全 | 用于 |
|---|---|---|---|
| GET | 是 | 是 | 检索资源 |
| POST | 否 | 否 | 创建资源、触发操作 |
| PUT | 是 | 否 | 完全替换资源 |
| PATCH | 否* | 否 | 部分更新资源 |
| DELETE | 是 | 否 | 删除资源 |
*PATCH 可以通过正确的实施实现幂等
# 成功
200 OK — GET、PUT、PATCH(带响应体)
201 Created — POST(包含 Location 头)
204 No Content — DELETE、PUT(无响应体)
# 客户端错误
400 Bad Request — 验证失败、格式错误的 JSON
401 Unauthorized — 缺少或无效的身份验证
403 Forbidden — 已身份验证但未授权
404 Not Found — 资源不存在
409 Conflict — 重复条目、状态冲突
422 Unprocessable Entity — 语义无效(有效 JSON、数据错误)
429 Too Many Requests — 超过速率限制
# 服务器错误
500 Internal Server Error — 意外失败(永远不要暴露详细信息)
502 Bad Gateway — 上游服务失败
503 Service Unavailable — 临时过载、包含 Retry-After
# 坏:所有内容都返回 200
{ "status": 200, "success": false, "error": "Not found" }
# 好:按语义使用 HTTP 状态代码
HTTP/1.1 404 Not Found
{ "error": { "code": "not_found", "message": "User not found" } }
# 坏:验证错误返回 500
# 好:返回 400 或 422 并包含字段级详细信息
# 坏:创建的资源返回 200
# 好:返回 201 并包含 Location 头
HTTP/1.1 201 Created
Location: /api/v1/users/abc-123
{
"data": {
"id": "abc-123",
"email": "alice@example.com",
"name": "Alice",
"created_at": "2025-01-15T10:30:00Z"
}
}
{
"data": [
{ "id": "abc-123", "name": "Alice" },
{ "id": "def-456", "name": "Bob" }
],
"meta": {
"total": 142,
"page": 1,
"per_page": 20,
"total_pages": 8
},
"links": {
"self": "/api/v1/users?page=1&per_page=20",
"next": "/api/v1/users?page=2&per_page=20",
"last": "/api/v1/users?page=8&per_page=20"
}
}
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"code": "invalid_format"
},
{
"field": "age",
"message": "Must be between 0 and 150",
"code": "out_of_range"
}
]
}
}
// 选项 A:带数据包装器的信封(推荐用于公共 API)
interface ApiResponse<T> {
data: T;
meta?: PaginationMeta;
links?: PaginationLinks;
}
interface ApiError {
error: {
code: string;
message: string;
details?: FieldError[];
};
}
// 选项 B:扁平响应(更简单,常见于内部 API)
// 成功:直接返回资源
// 错误:返回错误对象
// 通过 HTTP 状态代码区分
GET /api/v1/users?page=2&per_page=20
# 实施
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 20;
优点: 易于实施,支持"跳转到第 N 页" 缺点: 在大偏移量时慢(OFFSET 100000),与并发插入不一致
GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
# 实施
SELECT * FROM users
WHERE id > :cursor_id
ORDER BY id ASC
LIMIT 21; -- 多获取一个以确定 has_next
{
"data": [...],
"meta": {
"has_next": true,
"next_cursor": "eyJpZCI6MTQzfQ"
}
}
优点: 无论位置如何性能一致,与并发插入稳定 缺点: 无法跳转到任意页面,游标是不透明的
| 用例 | 分页类型 |
|---|---|
| 管理仪表板、小数据集(<10K) | 偏移量 |
| 无限滚动、信息流、大数据集 | 游标 |
| 公共 API | 游标(默认)+ 偏移量(可选) |
| 搜索结果 | 偏移量(用户期望页码) |
# 简单相等
GET /api/v1/orders?status=active&customer_id=abc-123
# 比较运算符(使用括号表示法)
GET /api/v1/products?price[gte]=10&price[lte]=100
GET /api/v1/orders?created_at[after]=2025-01-01
# 多个值(逗号分隔)
GET /api/v1/products?category=electronics,clothing
# 嵌套字段(点表示法)
GET /api/v1/orders?customer.country=US
# 单字段(前缀 - 表示降序)
GET /api/v1/products?sort=-created_at
# 多字段(逗号分隔)
GET /api/v1/products?sort=-featured,price,-created_at
# 搜索查询参数
GET /api/v1/products?q=wireless+headphones
# 字段特定搜索
GET /api/v1/users?email=alice
# 仅返回指定字段(减少有效负载)
GET /api/v1/users?fields=id,name,email
GET /api/v1/orders?fields=id,total,status&include=customer.name
# Authorization 头中的不记名令牌
GET /api/v1/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# API 密钥(用于服务器到服务器)
GET /api/v1/data
X-API-Key: sk_live_abc123
// 资源级:检查所有权
app.get("/api/v1/orders/:id", async (req, res) => {
const order = await Order.findById(req.params.id);
if (!order) return res.status(404).json({ error: { code: "not_found" } });
if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
return res.json({ data: order });
});
// 基于角色:检查权限
app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
await User.delete(req.params.id);
return res.status(204).send();
});
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000
# 超过时
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again in 60 seconds."
}
}
| 层级 | 限制 | 窗口 | 用例 |
|---|---|---|---|
| 匿名 | 30/分钟 | 每 IP | 公共端点 |
| 已身份验证 | 100/分钟 | 每用户 | 标准 API 访问 |
| 高级 | 1000/分钟 | 每 API 密钥 | 付费 API 计划 |
| 内部 | 10000/分钟 | 每服务 | 服务到服务 |
/api/v1/users
/api/v2/users
优点: 明确、易于路由、可缓存 缺点: 版本之间 URL 变化
GET /api/users
Accept: application/vnd.myapp.v2+json
优点: 干净的 URL 缺点: 更难测试、容易忘记
1. 从 /api/v1/ 开始 — 不需要时不版本控制
2. 最多维护 2 个活动版本(当前 + 以前)
3. 弃用时间表:
- 宣布弃用(公共 API 提前 6 个月通知)
- 添加 Sunset 头:Sunset: Sat, 01 Jan 2026 00:00:00 GMT
- 在日落日期后返回 410 Gone
4. 非破坏性更改不需要新版本:
- 向响应添加新字段
- 添加新的可选查询参数
- 添加新端点
5. 破坏性更改需要新版本:
- 删除或重命名字段
- 更改字段类型
- 更改 URL 结构
- 更改身份验证方法
import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
});
export async function POST(req: NextRequest) {
const body = await req.json();
const parsed = createUserSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({
error: {
code: "validation_error",
message: "Request validation failed",
details: parsed.error.issues.map(i => ({
field: i.path.join("."),
message: i.message,
code: i.code,
})),
},
}, { status: 422 });
}
const user = await createUser(parsed.data);
return NextResponse.json(
{ data: user },
{
status: 201,
headers: { Location: `/api/v1/users/${user.id}` },
},
);
}
from rest_framework import serializers, viewsets, status
from rest_framework.response import Response
class CreateUserSerializer(serializers.Serializer):
email = serializers.EmailField()
name = serializers.CharField(max_length=100)
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "email", "name", "created_at"]
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
if self.action == "create":
return CreateUserSerializer
return UserSerializer
def create(self, request):
serializer = CreateUserSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = UserService.create(**serializer.validated_data)
return Response(
{"data": UserSerializer(user).data},
status=status.HTTP_201_CREATED,
headers={"Location": f"/api/v1/users/{user.id}"},
)
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
return
}
if err := req.Validate(); err != nil {
writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
return
}
user, err := h.service.Create(r.Context(), req)
if err != nil {
switch {
case errors.Is(err, domain.ErrEmailTaken):
writeError(w, http.StatusConflict, "email_taken", "Email already registered")
default:
writeError(w, http.StatusInternalServerError, "internal_error", "Internal error")
}
return
}
w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID))
writeJSON(w, http.StatusCreated, map[string]any{"data": user})
}
在发布新端点之前: