api-design

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

API Design

API设计

Principles and patterns for designing APIs that are consistent, predictable, and easy to evolve. Applies to any language or framework — the focus is on protocol-level design decisions, not implementation details.
A well-designed API treats its surface as a product: consumers should be able to predict behavior, recover from errors, and integrate without reading source code.
设计一致、可预测且易于演进的API的原则与模式。适用于任何语言或框架——重点在于协议层面的设计决策,而非实现细节。
设计精良的API会将其接口视为产品:消费者应能预测行为、从错误中恢复,无需阅读源码即可完成集成。

When to Use

适用场景

  • Designing a new public or internal API from scratch
  • Reviewing an existing API for consistency and usability
  • Choosing between REST and GraphQL for a project
  • Planning API versioning or migration strategy
  • Defining error response contracts across services
  • Establishing API standards for a team or organization
  • 从零开始设计新的公共或内部API
  • 审查现有API的一致性与易用性
  • 为项目选择REST或GraphQL
  • 规划API版本控制或迁移策略
  • 定义跨服务的错误响应契约
  • 为团队或组织建立API标准

REST vs GraphQL

REST vs GraphQL

AspectRESTGraphQL
Best forCRUD-heavy, resource-oriented domainsComplex, interconnected data with varied client needs
Data fetchingFixed response shapes per endpointClient specifies exact fields needed
Over-fetchingCommon — endpoints return full resourcesEliminated — clients request only what they need
Under-fetchingCommon — requires multiple round tripsEliminated — single query can span relations
CachingBuilt-in HTTP caching (ETags, Cache-Control)Requires custom caching (normalized stores, persisted queries)
File uploadsNative multipart supportRequires workarounds (multipart spec or separate endpoint)
Real-timeWebhooks, SSE, or pollingSubscriptions built into the spec
Tooling maturityMature — OpenAPI, Postman, HTTP clientsGrowing — Apollo, Relay, GraphiQL
Learning curveLower — leverages existing HTTP knowledgeHigher — schema language, resolvers, query optimization
Error handlingHTTP status codes + response bodyAlways 200 — errors in response
errors
array
VersioningURL path, headers, or query paramsSchema evolution via deprecation + additive changes
Choose REST when: your domain maps naturally to resources and CRUD operations, you need HTTP caching, or your clients are simple (mobile apps, third-party integrations).
Choose GraphQL when: clients have highly varied data needs, you are aggregating multiple backend services, or you want a strongly typed contract between frontend and backend.
Both are valid. Many systems use REST for external/public APIs and GraphQL for internal frontend-backend communication.
维度RESTGraphQL
最适用场景以CRUD操作为主、面向资源的领域数据复杂且关联紧密、客户端需求多样的场景
数据获取每个端点返回固定结构的响应客户端指定所需的精确字段
过度获取常见——端点返回完整资源消除——客户端仅请求所需内容
获取不足常见——需要多次请求消除——单次查询可跨关联获取数据
缓存内置HTTP缓存(ETags、Cache-Control)需要自定义缓存(规范化存储、持久化查询)
文件上传原生支持多部分上传需要变通方案(多部分规范或独立端点)
实时性Webhooks、SSE或轮询规范内置订阅功能
工具成熟度成熟——OpenAPI、Postman、HTTP客户端持续发展中——Apollo、Relay、GraphiQL
学习曲线较低——利用现有HTTP知识较高——需要掌握模式语言、解析器、查询优化
错误处理HTTP状态码 + 响应体始终返回200——错误包含在响应的
errors
数组中
版本控制URL路径、请求头或查询参数通过废弃字段与增量变更实现模式演进
选择REST的场景:你的领域天然映射到资源与CRUD操作,需要HTTP缓存,或客户端较为简单(移动应用、第三方集成)。
选择GraphQL的场景:客户端数据需求差异大,需要聚合多个后端服务,或希望在前后端之间建立强类型契约。
两者均有效:许多系统对外/公共API使用REST,内部前后端通信使用GraphQL。

REST Design Principles

REST设计原则

REST APIs model the domain as resources and use HTTP semantics to operate on them.
Core rules:
  • Resources are nouns, not verbs:
    /orders
    , not
    /getOrders
  • HTTP methods are the verbs: GET reads, POST creates, PUT replaces, PATCH updates, DELETE removes
  • URLs identify resources; query parameters filter, sort, or paginate them
  • Use plural nouns for collections:
    /users
    ,
    /users/{id}
  • Limit nesting to two levels:
    /users/{id}/orders
    is fine;
    /users/{id}/orders/{id}/items/{id}/variants
    is not
  • Use HTTP status codes meaningfully — do not return 200 for everything
  • Support content negotiation via
    Accept
    and
    Content-Type
    headers
HATEOAS (Hypermedia as the Engine of Application State) adds discoverability by including links in responses. Useful for public APIs but often overkill for internal services:
json
{
  "id": 42,
  "status": "shipped",
  "_links": {
    "self": { "href": "/orders/42" },
    "cancel": { "href": "/orders/42/cancel", "method": "POST" },
    "customer": { "href": "/customers/7" }
  }
}
See REST Patterns Reference for detailed conventions.
REST API将领域建模为资源,并使用HTTP语义对其进行操作。
核心规则
  • 资源是名词而非动词:
    /orders
    ,而非
    /getOrders
  • HTTP方法作为动词:GET用于读取,POST用于创建,PUT用于替换,PATCH用于更新,DELETE用于删除
  • URL标识资源;查询参数用于过滤、排序或分页
  • 集合使用复数名词:
    /users
    /users/{id}
  • 嵌套层级限制为两级:
    /users/{id}/orders
    是合理的;
    /users/{id}/orders/{id}/items/{id}/variants
    则不合理
  • 有意义地使用HTTP状态码——不要所有情况都返回200
  • 通过
    Accept
    Content-Type
    请求头支持内容协商
HATEOAS(超媒体作为应用状态引擎)通过在响应中包含链接增加可发现性。对公共API有用,但对内部服务通常过于复杂:
json
{
  "id": 42,
  "status": "shipped",
  "_links": {
    "self": { "href": "/orders/42" },
    "cancel": { "href": "/orders/42/cancel", "method": "POST" },
    "customer": { "href": "/customers/7" }
  }
}
详细约定请参考REST模式参考

GraphQL Design Principles

GraphQL设计原则

GraphQL APIs expose a strongly typed schema that clients query declaratively.
Core rules:
  • Design schema-first — define the type system before writing resolvers
  • Types represent domain concepts; fields represent attributes and relations
  • Queries read data, mutations write data, subscriptions stream data
  • Use the type system to enforce constraints (non-null, enums, input types)
  • Avoid deeply nested schemas that create unpredictable query costs
  • Solve N+1 problems with batching (dataloader pattern)
  • Limit query depth and complexity to prevent abuse
Schema-first example:
graphql
type User {
  id: ID!
  name: String!
  email: String!
  orders(first: Int, after: String): OrderConnection!
}

type Order {
  id: ID!
  total: Float!
  status: OrderStatus!
  createdAt: DateTime!
}

enum OrderStatus {
  PENDING
  CONFIRMED
  SHIPPED
  DELIVERED
  CANCELLED
}
See GraphQL Patterns Reference for detailed conventions.
GraphQL API对外暴露强类型模式,客户端通过声明式方式查询。
核心规则
  • 优先设计模式——在编写解析器前先定义类型系统
  • 类型代表领域概念;字段代表属性与关联
  • 查询用于读取数据,变更用于写入数据,订阅用于流式传输数据
  • 使用类型系统强制执行约束(非空、枚举、输入类型)
  • 避免深度嵌套的模式,以免造成不可预测的查询成本
  • 通过批处理(dataloader模式)解决N+1问题
  • 限制查询深度与复杂度以防止滥用
模式优先示例
graphql
type User {
  id: ID!
  name: String!
  email: String!
  orders(first: Int, after: String): OrderConnection!
}

type Order {
  id: ID!
  total: Float!
  status: OrderStatus!
  createdAt: DateTime!
}

enum OrderStatus {
  PENDING
  CONFIRMED
  SHIPPED
  DELIVERED
  CANCELLED
}
详细约定请参考GraphQL模式参考

Error Handling

错误处理

A consistent error format is one of the most impactful API design decisions. Consumers should be able to parse errors programmatically without inspecting message strings.
RFC 7807 Problem Details format (recommended for REST):
json
{
  "type": "https://api.example.com/errors/insufficient-funds",
  "title": "Insufficient Funds",
  "status": 422,
  "detail": "Account balance is $10.00 but the transfer requires $50.00.",
  "instance": "/transfers/abc-123",
  "errors": [
    {
      "field": "amount",
      "code": "insufficient_funds",
      "message": "Transfer amount exceeds available balance"
    }
  ]
}
Key principles:
  • Use a machine-readable
    type
    or
    code
    — clients should branch on codes, not messages
  • Include a human-readable
    detail
    for debugging
  • Return field-level errors for validation failures so clients can highlight specific inputs
  • Use appropriate HTTP status codes (REST) or structured error types (GraphQL)
  • Never expose stack traces, internal paths, or SQL queries in production
  • Include a correlation/request ID for tracing errors across services
GraphQL error conventions:
json
{
  "data": { "createOrder": null },
  "errors": [
    {
      "message": "Insufficient funds",
      "extensions": {
        "code": "INSUFFICIENT_FUNDS",
        "field": "amount"
      }
    }
  ]
}
一致的错误格式是API设计中最具影响力的决策之一。消费者应能以编程方式解析错误,无需检查消息字符串。
RFC 7807问题详情格式(REST推荐)
json
{
  "type": "https://api.example.com/errors/insufficient-funds",
  "title": "Insufficient Funds",
  "status": 422,
  "detail": "Account balance is $10.00 but the transfer requires $50.00.",
  "instance": "/transfers/abc-123",
  "errors": [
    {
      "field": "amount",
      "code": "insufficient_funds",
      "message": "Transfer amount exceeds available balance"
    }
  ]
}
核心原则
  • 使用机器可读的
    type
    code
    ——客户端应基于代码分支,而非消息
  • 包含人类可读的
    detail
    用于调试
  • 验证失败时返回字段级错误,以便客户端高亮显示特定输入
  • 使用合适的HTTP状态码(REST)或结构化错误类型(GraphQL)
  • 生产环境中绝不暴露堆栈跟踪、内部路径或SQL查询
  • 包含关联/请求ID,以便跨服务追踪错误
GraphQL错误约定
json
{
  "data": { "createOrder": null },
  "errors": [
    {
      "message": "Insufficient funds",
      "extensions": {
        "code": "INSUFFICIENT_FUNDS",
        "field": "amount"
      }
    }
  ]
}

Versioning

版本控制

APIs evolve. Versioning strategies determine how you ship changes without breaking existing consumers.
StrategyMechanismProsCons
URL path
/v1/users
Explicit, easy to routeURL pollution, hard to deprecate
Accept header
Accept: application/vnd.api+json;version=2
Clean URLs, HTTP-correctLess visible, harder to test casually
Query param
/users?version=2
Simple to implementEasy to forget, caching complications
Practical guidance:
  • URL path versioning is the most common and easiest for consumers to understand
  • Only bump the major version for breaking changes
  • Prefer evolving the API additively (new fields, new endpoints) over creating new versions
  • When a version is deprecated, communicate a sunset date and provide migration guides
See API Evolution Reference for detailed strategies.
API会不断演进。版本控制策略决定了你如何在不影响现有消费者的情况下发布变更。
策略实现方式优点缺点
URL路径
/v1/users
明确,易于路由URL冗余,难以废弃旧版本
Accept请求头
Accept: application/vnd.api+json;version=2
URL简洁,符合HTTP规范可见性低,难以随意测试
查询参数
/users?version=2
实现简单容易遗漏,缓存复杂
实用指南
  • URL路径版本控制是最常见且最易于消费者理解的方式
  • 仅在发生破坏性变更时升级主版本号
  • 优先通过增量方式演进API(新增字段、新增端点),而非创建新版本
  • 当版本被废弃时,明确告知终止日期并提供迁移指南
详细策略请参考API演进参考

Pagination

分页

Every list endpoint needs pagination. The choice between cursor and offset affects performance, consistency, and client complexity.
ApproachHow it worksProsCons
Offset
?offset=20&limit=10
Simple, supports "jump to page N"Inconsistent with real-time inserts/deletes, slow on large tables
Cursor
?after=abc123&limit=10
Stable with real-time data, performant at scaleCannot jump to arbitrary pages
Best practices:
  • Set a maximum page size (e.g., 100) and a sensible default (e.g., 20)
  • Return pagination metadata:
    hasNextPage
    ,
    hasPreviousPage
    ,
    totalCount
    (if cheap to compute)
  • If
    totalCount
    is expensive, make it optional or return an estimate
  • Use cursors for feeds, activity streams, and any data that changes frequently
  • Use offset for admin dashboards, reports, and datasets that rarely change during browsing
Cursor pagination response example:
json
{
  "data": [ ... ],
  "pagination": {
    "hasNextPage": true,
    "hasPreviousPage": false,
    "startCursor": "eyJpZCI6MX0=",
    "endCursor": "eyJpZCI6MTB9"
  }
}
每个列表端点都需要分页。游标分页与偏移分页的选择会影响性能、一致性和客户端复杂度。
方式工作原理优点缺点
偏移分页
?offset=20&limit=10
简单,支持"跳转到第N页"实时插入/删除时数据不一致,大数据表查询缓慢
游标分页
?after=abc123&limit=10
实时数据场景下稳定,大规模数据下性能优异无法跳转到任意页面
最佳实践
  • 设置最大页面大小(如100)和合理的默认值(如20)
  • 返回分页元数据:
    hasNextPage
    hasPreviousPage
    totalCount
    (如果计算成本低)
  • 如果
    totalCount
    计算成本高,将其设为可选或返回估算值
  • 针对信息流、活动流及任何频繁变更的数据使用游标分页
  • 针对管理后台、报表及浏览期间极少变更的数据集使用偏移分页
游标分页响应示例
json
{
  "data": [ ... ],
  "pagination": {
    "hasNextPage": true,
    "hasPreviousPage": false,
    "startCursor": "eyJpZCI6MX0=",
    "endCursor": "eyJpZCI6MTB9"
  }
}

Authentication & Authorization

认证与授权

Authentication verifies identity (who are you?). Authorization verifies permissions (what can you do?).
MechanismUse caseNotes
API keysServer-to-server, simple integrationsEasy to implement; rotate regularly; never expose in client code
OAuth 2.0Third-party access, delegated permissionsIndustry standard; use Authorization Code + PKCE for SPAs/mobile
JWT (Bearer tokens)Stateless auth for microservicesInclude only essential claims; set short expiry; validate signature and claims
Session cookiesBrowser-based web appsPair with CSRF protection; use
Secure
,
HttpOnly
,
SameSite
flags
Best practices:
  • Always use HTTPS — no exceptions
  • Transmit tokens in
    Authorization: Bearer <token>
    header, not in query strings
  • Implement scopes/permissions for fine-grained access control
  • Return
    401 Unauthorized
    for missing/invalid credentials,
    403 Forbidden
    for insufficient permissions
  • Rate-limit authentication endpoints aggressively to prevent brute-force attacks
  • Support token refresh flows to avoid forcing re-authentication
认证验证身份(你是谁?)。授权验证权限(你能做什么?)。
机制适用场景注意事项
API密钥服务器到服务器、简单集成易于实现;定期轮换;绝不在客户端代码中暴露
OAuth 2.0第三方访问、委托权限行业标准;SPA/移动应用使用Authorization Code + PKCE模式
JWT(Bearer令牌)微服务的无状态认证仅包含必要声明;设置短有效期;验证签名与声明
会话Cookie基于浏览器的Web应用搭配CSRF防护;使用
Secure
HttpOnly
SameSite
标记
最佳实践
  • 始终使用HTTPS——无例外
  • Authorization: Bearer <token>
    请求头中传输令牌,而非查询字符串
  • 实现范围/权限以进行细粒度访问控制
  • 缺少/无效凭证返回
    401 Unauthorized
    ,权限不足返回
    403 Forbidden
  • 对认证端点进行严格的速率限制,防止暴力攻击
  • 支持令牌刷新流程,避免强制用户重新认证

Common Antipatterns

常见反模式

AntipatternProblemFix
Chatty APIClients need 10+ requests to render a pageAggregate related data; consider GraphQL or composite endpoints
God endpointSingle endpoint accepts wildly different payloads via flagsSplit into focused endpoints with clear semantics
Inconsistent namingMix of
snake_case
,
camelCase
, plural/singular
Pick one convention and enforce it project-wide
Missing paginationList endpoints return unbounded resultsAlways paginate collections; set max page size
Breaking changes without versioningRenaming or removing fields breaks clients silentlyUse versioning or additive-only evolution
Leaking internalsDatabase column names, auto-increment IDs in URLsMap to stable external identifiers (UUIDs, slugs)
Ignoring idempotencyRetrying a POST creates duplicate resourcesSupport idempotency keys for non-idempotent operations
200 for everythingErrors return HTTP 200 with an error bodyUse appropriate HTTP status codes
Timestamps without timezone
2024-01-15 14:30:00
is ambiguous
Always use ISO 8601 with timezone:
2024-01-15T14:30:00Z
反模式问题修复方案
聊天式API客户端需要10+次请求才能渲染页面聚合相关数据;考虑使用GraphQL或复合端点
上帝端点单个端点通过标志接受完全不同的负载拆分为多个专注的端点,语义清晰
命名不一致混合使用
snake_case
camelCase
、单复数
选择一种约定并在项目范围内强制执行
缺少分页列表端点返回无限制结果始终对集合进行分页;设置最大页面大小
无版本控制的破坏性变更重命名或删除字段导致客户端静默崩溃使用版本控制或仅通过增量变更演进API
暴露内部实现URL中包含数据库列名、内部自增ID映射为稳定的外部标识符(UUID、语义化别名)
忽略幂等性重试POST请求会创建重复资源为非幂等操作支持幂等性密钥
所有情况返回200错误返回HTTP 200并在响应体中包含错误信息使用合适的HTTP状态码
不带时区的时间戳
2024-01-15 14:30:00
存在歧义
始终使用带时区的ISO 8601格式:
2024-01-15T14:30:00Z

Quality Checklist

质量检查清单

Before shipping or reviewing an API, verify:
  • Resource naming is consistent (plural nouns, no verbs in URLs)
  • HTTP methods match semantics (GET is safe, PUT/DELETE are idempotent)
  • Every list endpoint is paginated with a max page size
  • Error responses use a consistent format with machine-readable codes
  • Authentication is required and uses HTTPS
  • Rate limiting is in place with appropriate headers
  • Breaking changes are versioned or avoided via additive evolution
  • Request/response examples exist for every endpoint
  • Timestamps use ISO 8601 with timezone
  • IDs are stable external identifiers, not internal auto-increments
  • CORS is configured for browser clients (if applicable)
  • Compression (gzip/brotli) is enabled for responses
  • API documentation is generated from the source of truth (OpenAPI schema, GraphQL introspection)
在发布或审查API前,验证以下内容:
  • 资源命名一致(复数名词,URL中无动词)
  • HTTP方法符合语义(GET是安全的,PUT/DELETE是幂等的)
  • 每个列表端点都已分页并设置最大页面大小
  • 错误响应使用一致格式,包含机器可读代码
  • 已启用认证并使用HTTPS
  • 已配置速率限制并返回相应请求头
  • 破坏性变更已进行版本控制或通过增量变更避免
  • 每个端点都存在请求/响应示例
  • 时间戳使用带时区的ISO 8601格式
  • ID是稳定的外部标识符,而非内部自增ID
  • 已为浏览器客户端配置CORS(如适用)
  • 已启用响应压缩(gzip/brotli)
  • API文档从可信源生成(OpenAPI模式、GraphQL自省)