关于 api-design-patterns
面向 API 开发者的设计参考技能,涵盖 REST、GraphQL 和 gRPC。内容帮助选择 API 风格,并规划安全、版本控制、分页与错误处理。
主要功能
- 01REST、GraphQL 与 gRPC 指南
- 02API 风格选择依据
- 03版本控制与弃用模式
- 04身份验证与速率限制
- 05分页与幂等性模式
适用场景
- 01为新服务选择 API 风格
- 02规划兼容旧版的 API 变更
- 03设计分页与安全重试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill api-design-patterns安装后在 Claude Code 中输入/api-design-patterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.jsonreferences/authentication.mdgraphql-patterns.mdgrpc-patterns.mdrest-patterns.mdversioning-strategies.md详细介绍
API 设计模式
使用 REST、GraphQL 和 gRPC 的成熟模式设计健壮、可扩展的 API,并妥善处理版本控制、身份验证和错误处理。
快速参考
API 风格选择:
- REST:基于资源的 CRUD、简单的客户端、HTTP 原生缓存
- GraphQL:客户端驱动的查询、复杂的数据图、实时订阅
- gRPC:高性能 RPC、微服务、强类型、流式传输
关键模式:
- 版本控制:URI(
/v1/users)、标头(Accept: application/vnd.api+json;version=1)、内容协商 - 分页:偏移量(简单)、游标(稳定)、键集(高性能)
- 身份验证:OAuth2(委托)、JWT(无状态)、API 密钥(服务间)
- 速率限制:令牌桶、固定窗口、滑动窗口
- 幂等性:幂等键、条件请求、安全重试
深入了解请参阅 references/:rest-patterns.md、graphql-patterns.md、grpc-patterns.md、versioning-strategies.md、authentication.md
核心原则
通用 API 设计标准
在所有 API 风格中遵循以下原则:
1. 一致性优先于巧思
- 遵循 API 风格的既定约定
- 使用可预测的命名模式(snake_case 或 camelCase,选定一种)
- 保持错误响应格式一致
- 对破坏性变更进行版本控制,绝不让客户端措手不及
2. 为演进而设计
- 从第一天起就规划版本控制
- 使用具有合理默认值的可选字段
- 通过设置弃用日期平稳弃用
- 记录破坏性与非破坏性变更
3. 默认安全
- 除非明确为公开接口,否则要求身份验证
- 所有生产端点均使用 HTTPS/TLS
- 实施速率限制和节流
- 验证并清理所有输入
- 向客户端返回最少的错误详情
4. 开发者体验优先
- 提供完整的文档(OpenAPI、GraphQL 模式)
- 返回有意义且可供采取行动的错误消息
- 正确使用标准 HTTP 状态码
- 包含用于调试的请求 ID
- 提供 SDK 和代码生成器
API 风格决策树
何时选择 REST
✅ 适用于以下情况:
- 构建以 CRUD 为主的资源 API
- 客户端需要 HTTP 缓存(ETags、Cache-Control)
- 需要广泛的平台兼容性(浏览器、移动设备、IoT)
- 简单的无状态客户端—服务器模型即可满足需求
- 团队熟悉 HTTP/REST 约定
❌ 以下情况应避免使用 REST:
- 需要获取具有嵌套关系的复杂数据(N+1 查询)
- 实时更新是主要用例
- 需要强类型和代码生成
- 微服务之间需要高性能 RPC
示例用例:公共 API、移动后端、传统 Web 服务
何时选择 GraphQL
✅ 适用于以下情况:
- 客户端需要灵活的客户端驱动查询
- 数据图复杂且具有嵌套关系
- 多种客户端类型有不同的数据需求
- 需要实时订阅
- 需要强类型和模式验证
❌ 以下情况应避免使用 GraphQL:
- 简单的 CRUD 操作为主
- HTTP 缓存至关重要(GraphQL 使用 POST)
- 文件上传是主要功能(需要扩展)
- 团队缺乏 GraphQL 专业知识
- 性能优化较复杂(N+1 问题)
示例用例:面向客户端的 API、仪表板、UI 各异的移动应用
何时选择 gRPC
✅ 适用于以下情况:
- 微服务之间的通信
- 高性能和低延迟至关重要
- 需要双向流式传输
- 使用 Protocol Buffers 实现强类型
- 多语言环境(语言互操作)
❌ 以下情况应避免使用 gRPC:
- 浏览器客户端(支持有限,需要 grpc-web)
- 为兼容性需要 HTTP/JSON
- 倾向使用人类可读的负载
- 简单的请求/响应模式
示例用例:内部微服务、流式数据、服务网格
REST API 模式
资源命名
✅ 良好:使用复数名词,采用层级结构
GET /users # List users
GET /users/123 # Get user
POST /users # Create user
PUT /users/123 # Update user (full)
PATCH /users/123 # Update user (partial)
DELETE /users/123 # Delete user
GET /users/123/orders # User's orders (sub-resource)
❌ 不佳:使用动词,约定混杂
GET /getUsers # Don't use verbs
POST /user/create # Don't use verbs
GET /Users/123 # Don't capitalize
GET /user/123 # Don't mix singular/plural
HTTP 状态代码
成功代码:
200 OK:GET、PUT、PATCH、DELETE 请求成功且带有响应正文201 Created:POST 请求成功,返回 Location 标头202 Accepted:异步操作已启动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:暂时不可用504 Gateway Timeout:上游超时
错误响应格式
✅ 一致的错误结构
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_FORMAT"
}
],
"request_id": "req_abc123",
"documentation_url": "https://api.example.com/docs/errors/validation"
}
}
分页模式
偏移量分页(简单、常见):
GET /users?limit=20&offset=40
✅ 适用于:小型数据集、管理界面 ❌ 不适用于:大型数据集(跳过记录的成本会很高)、实时数据
游标分页(稳定、高效):
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ
Response: { "data": [...], "next_cursor": "eyJpZCI6MTQzfQ" }
✅ 适用于:无限滚动、实时信息流、大型数据集 ❌ 不适用于:随机访问、页码
键集分页(高性能):
GET /users?limit=20&after_id=123
✅ 适用于:有序数据、适合数据库索引 ❌ 不适用于:复杂排序、多个排序键
有关筛选、排序、字段选择、HATEOAS,请参阅 references/rest-patterns.md
GraphQL 模式
Schema 设计
✅ 良好:类型清晰,默认可为空
type User {
id: ID! # Non-null ID
email: String! # Required field
name: String # Optional (nullable by default)
createdAt: DateTime!
orders: [Order!]! # Non-null array of non-null orders
}
type Query {
user(id: ID!): User
users(first: Int, after: String): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
input CreateUserInput {
email: String!
name: String
}
type CreateUserPayload {
user: User
userEdge: UserEdge
errors: [UserError!]
}
Resolver 模式
使用 DataLoader 避免 N+1 查询:
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds: string[]) => {
const users = await db.users.findMany({ where: { id: { in: userIds } } });
return userIds.map(id => users.find(u => u.id === id));
});
// Resolver batches queries automatically
const resolvers = {
Order: {
user: (order) => userLoader.load(order.userId)
}
};
查询复杂度分析
防止执行开销过高的查询:
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
schema,
validationRules: [
createComplexityLimitRule(1000, {
onCost: (cost) => console.log('Query cost:', cost),
}),
],
});
有关订阅、Relay 游标连接和错误处理,请参阅 references/graphql-patterns.md
gRPC 模式
服务定义
syntax = "proto3";
package users.v1;
service UserService {
rpc GetUser (GetUserRequest) returns (User) {}
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse) {}
rpc CreateUser (CreateUserRequest) returns (User) {}
rpc StreamUsers (StreamUsersRequest) returns (stream User) {}
rpc BidiChat (stream ChatMessage) returns (stream ChatMessage) {}
}
message User {
string id = 1;
string email = 2;
string name = 3;
google.protobuf.Timestamp created_at = 4;
}
message GetUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}
错误处理
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
if req.Id == "" {
return nil, status.Error(codes.InvalidArgument, "user ID is required")
}
user, err := s.db.GetUser(ctx, req.Id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, status.Error(codes.NotFound, "user not found")
}
return nil, status.Error(codes.Internal, "database error")
}
return user, nil
}
有关流式传输、拦截器、元数据和健康检查,请参阅 references/grpc-patterns.md
版本控制策略
URI 版本控制(简单、明确)
✅ 最常见,易于理解
GET /v1/users/123
GET /v2/users/123
优点:清晰、易于路由、适合浏览器 缺点:版本与 URL 耦合、路由重复
标头版本控制(URL 简洁)
GET /users/123
Accept: application/vnd.myapi.v2+json
优点:URL 简洁,版本与资源分离 缺点:不够显眼,手动测试较困难
内容协商(粒度精细)
GET /users/123
Accept: application/vnd.myapi.user.v2+json
优点:资源级版本控制、向后兼容 缺点:复杂,实现难度较高
版本弃用流程
{
"version": "1.0",
"deprecated": true,
"sunset_date": "2025-12-31",
"migration_guide": "https://docs.api.com/v1-to-v2",
"replacement_version": "2.0"
}
包含弃用警告:
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Link: <https://docs.api.com/v1-to-v2>; rel="deprecation"
有关详细的迁移模式,请参阅 references/versioning-strategies.md
身份验证与授权
OAuth 2.0(委托访问)
适用于:第三方访问、用户同意、令牌刷新
授权码流程(对 Web/移动端最安全):
1. Client redirects to /authorize
2. User authenticates, grants permissions
3. Auth server redirects to callback with code
4. Client exchanges code for access token
5. Client uses access token for API requests
# Request token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://client.com/callback
&client_id=CLIENT_ID
&client_secret=CLIENT_SECRET
# Response
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read write"
}
# Use token
GET /v1/users/me
Authorization: Bearer eyJhbGc...
JWT(无状态身份验证)
适用于:微服务、无状态 API 身份验证、短期令牌
✅ 良好:声明精简、过期时间短
{
"sub": "user_123",
"iat": 1516239022,
"exp": 1516242622,
"scope": "read:users write:orders"
}
验证:
import jwt from 'jsonwebtoken';
const token = req.headers.authorization?.split(' ')[1];
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.userId = payload.sub;
API 密钥(服务间调用)
适用于:服务器到服务器、CLI 工具、Webhooks
GET /v1/users
X-API-Key: sk_live_abc123...
# Or query parameter (less secure)
GET /v1/users?api_key=sk_live_abc123
密钥实践:
- 为密钥添加环境前缀(
sk_live_、sk_test_) - 存储前先对密钥进行哈希处理(bcrypt、scrypt)
- 支持密钥轮换且不中断服务
- 支持每位用户使用多个密钥
- 按密钥实施速率限制
有关 API 密钥轮换、作用域和 RBAC,请参阅 references/authentication.md
速率限制
令牌桶(支持突发流量)
Bucket: 100 tokens, refill 10/second
Request costs 1 token
Allows bursts up to bucket size
标头:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1640995200
429 响应:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again in 60 seconds.",
"limit": 100,
"reset_at": "2025-01-01T00:00:00Z"
}
}
滑动窗口(公平分配)
统计滚动时间窗口内的请求数。比固定窗口更准确。
按用户与按 IP
- 按用户:已通过身份验证的请求,配额公平
- 按 IP:未经身份验证的请求,防止滥用
- 组合:同时应用两种限制,采用更严格的一种
幂等性
幂等方法(HTTP 规范)
天然幂等:GET、PUT、DELETE、HEAD、OPTIONS 非幂等:POST、PATCH
幂等键
使 POST 请求具备幂等性:
POST /v1/payments
Idempotency-Key: uuid-or-client-generated-key
Content-Type: application/json
{
"amount": 1000,
"currency": "USD",
"customer": "cust_123"
}
服务端行为:
- 首次请求:处理请求,并使用该键存储结果
- 重复请求(键相同):返回已存储的结果(200 或 201)
- 不同请求(键相同):返回 409 Conflict
实现:
const idempotencyKey = req.headers['idempotency-key'];
if (idempotencyKey) {
const cached = await redis.get(`idempotency:${idempotencyKey}`);
if (cached) {
return res.status(cached.status).json(cached.body);
}
}
const result = await processPayment(req.body);
await redis.setex(`idempotency:${idempotencyKey}`, 86400, {
status: 201,
body: result
});
条件请求
使用 ETag 安全地更新资源:
# Get resource with ETag
GET /v1/users/123
Response: ETag: "abc123"
# Update only if unchanged
PUT /v1/users/123
If-Match: "abc123"
# 412 Precondition Failed if ETag changed
缓存策略
HTTP 缓存标头
# Public, cacheable for 1 hour
Cache-Control: public, max-age=3600
# Private (user-specific), revalidate
Cache-Control: private, must-revalidate, max-age=0
# No caching
Cache-Control: no-store, no-cache, must-revalidate
ETag 验证
# Server returns ETag
GET /v1/users/123
Response:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: max-age=3600
# Client conditional request
GET /v1/users/123
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# 304 Not Modified if unchanged (saves bandwidth)
HTTP/1.1 304 Not Modified
Last-Modified
GET /v1/users/123
Response:
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
# Conditional request
GET /v1/users/123
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
# 304 Not Modified if not modified
Webhook
事件传递
POST https://client.com/webhooks/payments
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
X-Webhook-Id: evt_abc123
X-Webhook-Timestamp: 1640995200
{
"id": "evt_abc123",
"type": "payment.succeeded",
"created": 1640995200,
"data": {
"object": {
"id": "pay_123",
"amount": 1000,
"status": "succeeded"
}
}
}
签名验证
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expectedSignature}`)
);
}
重试策略
- 指数退避:1s、2s、4s、8s、16s、32s、64s
- 超时:每次尝试 5-30 秒
- 最大尝试次数:3-7 次
- 死信队列:存储失败的事件
- 手动重试:提供用于重新发送失败事件的界面
API 文档
OpenAPI/Swagger(REST)
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
components:
schemas:
User:
type: object
required: [id, email]
properties:
id:
type: string
email:
type: string
format: email
name:
type: string
GraphQL Schema(自描述)
GraphQL 内省可自动生成文档。请使用描述:
"""
Represents a user account in the system.
Created via the createUser mutation.
"""
type User {
"""Unique identifier for the user"""
id: ID!
"""Email address, must be unique"""
email: String!
"""Optional display name"""
name: String
}
API 文档最佳实践
- 交互式示例:提供可运行的代码示例
- 身份验证指南:逐步说明身份验证设置
- 错误目录:记录所有错误代码并附上示例
- 速率限制:清楚说明限制和标头
- 变更日志:跟踪破坏性和非破坏性变更
- 迁移指南:提供版本升级说明
- SDK:为常用语言提供客户端库
反模式
❌ 过度获取(REST):返回未使用字段的完整对象
✅ 解决方案:支持字段选择(?fields=id,name,email)
❌ 获取不足(REST):获取相关数据需要多次请求
✅ 解决方案:支持展开(?expand=orders,profile)或使用 GraphQL
❌ 过于繁琐的 API:常见操作需要过多往返请求 ✅ 解决方案:使用批量端点、复合文档或 GraphQL
❌ 忽视 HTTP 语义:使用 GET 执行修改操作,或使用错误的状态码 ✅ 解决方案:遵循 HTTP 规范,使用正确的方法和状态码
❌ 暴露内部结构:URL/Schema 与数据库结构相对应 ✅ 解决方案:设计独立于存储结构、以资源为中心的 API
❌ 缺少版本管理:破坏性变更没有提升版本号 ✅ 解决方案:从一开始就进行版本管理,绝不破坏现有版本
❌ 错误消息不佳:笼统地显示“发生错误” ✅ 解决方案:提供具体、可操作并带有代码的错误消息
❌ 没有速率限制:API 容易遭到滥用 ✅ 解决方案:从一开始就实施速率限制
测试策略
契约测试
// Pact contract test
import { PactV3 } from '@pact-foundation/pact';
const provider = new PactV3({
consumer: 'FrontendApp',
provider: 'UserAPI'
});
it('gets a user by ID', () => {
provider
.given('user 123 exists')
.uponReceiving('a request for user 123')
.withRequest({
method: 'GET',
path: '/users/123'
})
.willRespondWith({
status: 200,
body: { id: '123', email: '[email protected]' }
});
});
负载测试
// k6 load test
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 20 },
{ duration: '10s', target: 0 }
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% under 500ms
http_req_failed: ['rate<0.01'] // <1% errors
}
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500
});
}
相关技能
- graphql:深入了解 GraphQL Schema 设计、解析器、Apollo Server
- typescript:类型安全的 API 客户端和服务端
- nodejs-backend:Express/Fastify REST API 实现
- django:Django REST Framework 模式
- fastapi:FastAPI Python REST/GraphQL API
- flask:Flask-RESTful 模式
参考资料
- rest-patterns.md:深入介绍 REST(HATEOAS、筛选、字段选择)
- graphql-patterns.md:GraphQL 订阅、Relay 游标连接、联邦
- grpc-patterns.md:流式处理模式、拦截器、服务网格集成
- versioning-strategies.md:详细的版本管理方法和迁移模式
- authentication.md:OAuth 流程、JWT 最佳实践、API 密钥轮换、RBAC
其他资源
- REST API Design Rulebook - O'Reilly REST 指南
- GraphQL Best Practices - GraphQL 官方指南
- gRPC Best Practices - gRPC 官方指南
- RFC 7807: Problem Details for HTTP APIs - 标准错误格式
- OpenAPI Specification - REST 文档标准