mcpField
A
api-design-patterns

api-design-patterns

github.com/bobmatnyc开发78574 次安装15 周前更新
代码仓库

关于 api-design-patterns

面向 API 开发者的设计参考技能,涵盖 REST、GraphQL 和 gRPC。内容帮助选择 API 风格,并规划安全、版本控制、分页与错误处理。

主要功能

  1. 01REST、GraphQL 与 gRPC 指南
  2. 02API 风格选择依据
  3. 03版本控制与弃用模式
  4. 04身份验证与速率限制
  5. 05分页与幂等性模式

适用场景

  1. 01为新服务选择 API 风格
  2. 02规划兼容旧版的 API 变更
  3. 03设计分页与安全重试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill api-design-patterns

安装后在 Claude Code 中输入/api-design-patterns确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.json
references/
authentication.md
graphql-patterns.md
grpc-patterns.md
rest-patterns.md
versioning-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"
}

服务端行为:

  1. 首次请求:处理请求,并使用该键存储结果
  2. 重复请求(键相同):返回已存储的结果(200 或 201)
  3. 不同请求(键相同):返回 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 文档最佳实践
  1. 交互式示例:提供可运行的代码示例
  2. 身份验证指南:逐步说明身份验证设置
  3. 错误目录:记录所有错误代码并附上示例
  4. 速率限制:清楚说明限制和标头
  5. 变更日志:跟踪破坏性和非破坏性变更
  6. 迁移指南:提供版本升级说明
  7. 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

其他资源

评论

支持 Markdown