api-design
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAPI 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
| Aspect | REST | GraphQL |
|---|---|---|
| Best for | CRUD-heavy, resource-oriented domains | Complex, interconnected data with varied client needs |
| Data fetching | Fixed response shapes per endpoint | Client specifies exact fields needed |
| Over-fetching | Common — endpoints return full resources | Eliminated — clients request only what they need |
| Under-fetching | Common — requires multiple round trips | Eliminated — single query can span relations |
| Caching | Built-in HTTP caching (ETags, Cache-Control) | Requires custom caching (normalized stores, persisted queries) |
| File uploads | Native multipart support | Requires workarounds (multipart spec or separate endpoint) |
| Real-time | Webhooks, SSE, or polling | Subscriptions built into the spec |
| Tooling maturity | Mature — OpenAPI, Postman, HTTP clients | Growing — Apollo, Relay, GraphiQL |
| Learning curve | Lower — leverages existing HTTP knowledge | Higher — schema language, resolvers, query optimization |
| Error handling | HTTP status codes + response body | Always 200 — errors in response |
| Versioning | URL path, headers, or query params | Schema 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.
| 维度 | REST | GraphQL |
|---|---|---|
| 最适用场景 | 以CRUD操作为主、面向资源的领域 | 数据复杂且关联紧密、客户端需求多样的场景 |
| 数据获取 | 每个端点返回固定结构的响应 | 客户端指定所需的精确字段 |
| 过度获取 | 常见——端点返回完整资源 | 消除——客户端仅请求所需内容 |
| 获取不足 | 常见——需要多次请求 | 消除——单次查询可跨关联获取数据 |
| 缓存 | 内置HTTP缓存(ETags、Cache-Control) | 需要自定义缓存(规范化存储、持久化查询) |
| 文件上传 | 原生支持多部分上传 | 需要变通方案(多部分规范或独立端点) |
| 实时性 | Webhooks、SSE或轮询 | 规范内置订阅功能 |
| 工具成熟度 | 成熟——OpenAPI、Postman、HTTP客户端 | 持续发展中——Apollo、Relay、GraphiQL |
| 学习曲线 | 较低——利用现有HTTP知识 | 较高——需要掌握模式语言、解析器、查询优化 |
| 错误处理 | HTTP状态码 + 响应体 | 始终返回200——错误包含在响应的 |
| 版本控制 | 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: , not
/orders/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: is fine;
/users/{id}/ordersis not/users/{id}/orders/{id}/items/{id}/variants - Use HTTP status codes meaningfully — do not return 200 for everything
- Support content negotiation via and
AcceptheadersContent-Type
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 or
type— clients should branch on codes, not messagescode - Include a human-readable for debugging
detail - 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.
| Strategy | Mechanism | Pros | Cons |
|---|---|---|---|
| URL path | | Explicit, easy to route | URL pollution, hard to deprecate |
| Accept header | | Clean URLs, HTTP-correct | Less visible, harder to test casually |
| Query param | | Simple to implement | Easy 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路径 | | 明确,易于路由 | URL冗余,难以废弃旧版本 |
| Accept请求头 | | URL简洁,符合HTTP规范 | 可见性低,难以随意测试 |
| 查询参数 | | 实现简单 | 容易遗漏,缓存复杂 |
实用指南:
- URL路径版本控制是最常见且最易于消费者理解的方式
- 仅在发生破坏性变更时升级主版本号
- 优先通过增量方式演进API(新增字段、新增端点),而非创建新版本
- 当版本被废弃时,明确告知终止日期并提供迁移指南
详细策略请参考API演进参考。
Pagination
分页
Every list endpoint needs pagination. The choice between cursor and offset affects performance, consistency, and client complexity.
| Approach | How it works | Pros | Cons |
|---|---|---|---|
| Offset | | Simple, supports "jump to page N" | Inconsistent with real-time inserts/deletes, slow on large tables |
| Cursor | | Stable with real-time data, performant at scale | Cannot 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(if cheap to compute)totalCount - If is expensive, make it optional or return an estimate
totalCount - 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"
}
}每个列表端点都需要分页。游标分页与偏移分页的选择会影响性能、一致性和客户端复杂度。
| 方式 | 工作原理 | 优点 | 缺点 |
|---|---|---|---|
| 偏移分页 | | 简单,支持"跳转到第N页" | 实时插入/删除时数据不一致,大数据表查询缓慢 |
| 游标分页 | | 实时数据场景下稳定,大规模数据下性能优异 | 无法跳转到任意页面 |
最佳实践:
- 设置最大页面大小(如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?).
| Mechanism | Use case | Notes |
|---|---|---|
| API keys | Server-to-server, simple integrations | Easy to implement; rotate regularly; never expose in client code |
| OAuth 2.0 | Third-party access, delegated permissions | Industry standard; use Authorization Code + PKCE for SPAs/mobile |
| JWT (Bearer tokens) | Stateless auth for microservices | Include only essential claims; set short expiry; validate signature and claims |
| Session cookies | Browser-based web apps | Pair with CSRF protection; use |
Best practices:
- Always use HTTPS — no exceptions
- Transmit tokens in header, not in query strings
Authorization: Bearer <token> - Implement scopes/permissions for fine-grained access control
- Return for missing/invalid credentials,
401 Unauthorizedfor insufficient permissions403 Forbidden - 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防护;使用 |
最佳实践:
- 始终使用HTTPS——无例外
- 在请求头中传输令牌,而非查询字符串
Authorization: Bearer <token> - 实现范围/权限以进行细粒度访问控制
- 缺少/无效凭证返回,权限不足返回
401 Unauthorized403 Forbidden - 对认证端点进行严格的速率限制,防止暴力攻击
- 支持令牌刷新流程,避免强制用户重新认证
Common Antipatterns
常见反模式
| Antipattern | Problem | Fix |
|---|---|---|
| Chatty API | Clients need 10+ requests to render a page | Aggregate related data; consider GraphQL or composite endpoints |
| God endpoint | Single endpoint accepts wildly different payloads via flags | Split into focused endpoints with clear semantics |
| Inconsistent naming | Mix of | Pick one convention and enforce it project-wide |
| Missing pagination | List endpoints return unbounded results | Always paginate collections; set max page size |
| Breaking changes without versioning | Renaming or removing fields breaks clients silently | Use versioning or additive-only evolution |
| Leaking internals | Database column names, auto-increment IDs in URLs | Map to stable external identifiers (UUIDs, slugs) |
| Ignoring idempotency | Retrying a POST creates duplicate resources | Support idempotency keys for non-idempotent operations |
| 200 for everything | Errors return HTTP 200 with an error body | Use appropriate HTTP status codes |
| Timestamps without timezone | | Always use ISO 8601 with timezone: |
| 反模式 | 问题 | 修复方案 |
|---|---|---|
| 聊天式API | 客户端需要10+次请求才能渲染页面 | 聚合相关数据;考虑使用GraphQL或复合端点 |
| 上帝端点 | 单个端点通过标志接受完全不同的负载 | 拆分为多个专注的端点,语义清晰 |
| 命名不一致 | 混合使用 | 选择一种约定并在项目范围内强制执行 |
| 缺少分页 | 列表端点返回无限制结果 | 始终对集合进行分页;设置最大页面大小 |
| 无版本控制的破坏性变更 | 重命名或删除字段导致客户端静默崩溃 | 使用版本控制或仅通过增量变更演进API |
| 暴露内部实现 | URL中包含数据库列名、内部自增ID | 映射为稳定的外部标识符(UUID、语义化别名) |
| 忽略幂等性 | 重试POST请求会创建重复资源 | 为非幂等操作支持幂等性密钥 |
| 所有情况返回200 | 错误返回HTTP 200并在响应体中包含错误信息 | 使用合适的HTTP状态码 |
| 不带时区的时间戳 | | 始终使用带时区的ISO 8601格式: |
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自省)