with one click
api-design
生产级 API 的 REST API 设计模式,包括资源命名、状态码、分页、过滤、错误响应、版本控制和速率限制。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
生产级 API 的 REST API 设计模式,包括资源命名、状态码、分页、过滤、错误响应、版本控制和速率限制。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
撰写文章、指南、博客、教程、时事通讯等长篇内容,并根据提供的示例或品牌指南呈现独特的语言风格。当用户需要篇幅超过一段的精炼文字,且重视风格一致性、结构和可信度时,请使用此技能。
后端架构模式、API 设计、数据库优化以及适用于 Node.js、Express 和 Next.js API 路由的服务端最佳实践。
适用于 TypeScript、JavaScript、React 和 Node.js 开发的通用编码标准、最佳实践与模式。
为 X、LinkedIn、TikTok、YouTube、时事通讯 (Newsletters) 以及多平台复用活动创建平台原生内容系统。当用户需要社交帖子、推文串 (Threads)、脚本、内容日历或将单一源素材清晰地适配到多个平台时使用。
Playwright E2E 测试模式、页面对象模型(POM)、配置、CI/CD 集成、产物管理以及不稳定测试(flaky test)策略。
适用于 Claude Code 会话的正规评测框架(Evaluation Framework),实现了评测驱动开发(Eval-Driven Development, EDD)原则
| name | api-design |
| description | 生产级 API 的 REST API 设计模式,包括资源命名、状态码、分页、过滤、错误响应、版本控制和速率限制。 |
| origin | ECC |
用于设计一致且开发者友好的 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
# 推荐(GOOD)
/api/v1/team-members # 多词资源使用短横线命名法(kebab-case)
/api/v1/orders?status=active # 使用查询参数进行过滤
/api/v1/users/123/orders # 嵌套资源表示归属关系
# 错误(BAD)
/api/v1/getUsers # URL 中包含动词
/api/v1/user # 使用单数(应使用复数)
/api/v1/team_members # URL 中使用蛇形命名法(snake_case)
/api/v1/users/123/getOrders # 嵌套资源中包含动词
| 方法 | 幂等性(Idempotent) | 安全性(Safe) | 用途 |
|---|---|---|---|
| GET | 是 | 是 | 获取资源 |
| POST | 否 | 否 | 创建资源,触发操作 |
| PUT | 是 | 否 | 完整替换资源 |
| PATCH | 否* | 否 | 部分更新资源 |
| DELETE | 是 | 否 | 删除资源 |
*通过正确的实现,PATCH 也可以设计为幂等。
# 成功(Success)
200 OK — GET, PUT, PATCH(包含响应体)
201 Created — POST(需包含 Location 响应头)
204 No Content — DELETE, PUT(不含响应体)
# 客户端错误(Client Errors)
400 Bad Request — 校验失败,JSON 格式错误
401 Unauthorized — 缺失或无效的身份验证
403 Forbidden — 已验证身份但未获得授权
404 Not Found — 资源不存在
409 Conflict — 重复条目,状态冲突
422 Unprocessable Entity — 语义错误(JSON 正确但数据非法)
429 Too Many Requests — 超出速率限制
# 服务端错误(Server Errors)
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:包含 data 包装器的信封模式(推荐用于公开 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) | 偏移量(Offset) |
| 无限滚动、Feed 流、大型数据集 | 游标(Cursor) |
| 公开 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 标头中的 Bearer 令牌
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."
}
}
| 层级 | 限制 | 窗口(Window) | 使用场景 |
|---|---|---|---|
| 匿名用户 | 30/min | 按 IP | 公开端点 |
| 已验证用户 | 100/min | 按用户 | 标准 API 访问 |
| 高级用户 | 1000/min | 按 API 密钥 | 付费 API 套餐 |
| 内部调用 | 10000/min | 按服务 | 服务间通信 |
/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
- 在 Sunset 日期后返回 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})
}
在发布新端点之前: