Postman 2023年的报告显示:75%的受访者认同API-first公司的开发者更高效、软件质量更好。与此同时,52%的开发者将"缺乏文档"列为消费API的最大障碍。这些数字背后是一个简单的真相:你是否用过那种让你怀疑人生的API?POST /getUsers、所有错误都返回200、分页用page=1,2,3却在第5页突然跳过两条数据……

问题不在于技术栈,而在于设计决策。一个设计良好的API和糟糕的API,区别往往在于是否遵循了经过验证的原则,以及是否避免了一些极其常见却危害巨大的反模式。

从"外向内"设计,而不是"内向外"

最根本的错误发生在写代码之前:从错误的角度设计API。

泄漏抽象的代价

“内向外”(Inside-out)的设计从内部系统和数据模型出发,然后直接暴露给API。这感觉很自然——需要更少的转换工作——但它创造了一个泄漏的抽象,强迫消费者理解你的内部结构。

比较这两个图书馆API端点:

// 内向外设计(有问题)
GET /api/database/tables/book_inventory/records?status=1

// 外向内设计(更好)
GET /api/books?available=true

第一个暴露了数据库实现细节。第二个关注开发者真正需要的——找到可借阅的书。

学习业界标杆

Stripe的API文档是外向内设计的典范。他们痴迷地关注开发者体验,投入大量资源在多语言SDK和交互式文档上。这种方法的回报是显而易见的——开发者普遍认为Stripe拥有最好的API体验之一,直接促成了他们在激烈竞争中的市场主导地位。

外向内方法意味着像API消费者一样思考。问自己:“如果我对内部系统一无所知,什么样的设计对我来说最合理?“这种转变防止你创建强迫消费者学习内部领域语言和结构的API。

URL设计的两个铁律

你的API的URL结构提供了API质量和可用性的第一印象。

铁律一:用名词,不要用动词

许多开发者创建反映存储机制而非有意义资源的URL:

// 错误示范
GET  /api/getUsers
POST /api/createOrder
PUT  /api/updateProduct/123
DELETE /api/deleteCustomer/456

// 正确做法
GET    /api/users
POST   /api/orders
PUT    /api/products/123
DELETE /api/customers/456

第一种方法在端点中使用动词,混合了命名约定。第二种正确地使用名词表示资源,让HTTP方法传达动作。

铁律二:避免过度嵌套

另一个常见错误是创建过于复杂的资源层级:

// 过度嵌套(有问题)
GET /api/companies/456/departments/2/employees/123/projects

// 更灵活的设计
GET /api/employees/123/projects
GET /api/projects?employeeId=123

根据Microsoft的API设计指南,过度嵌套使API变得脆弱且难以演进。第二种方法根据消费者需求提供多种访问相同数据的方式,在不牺牲清晰度的前提下提高了灵活性。

HTTP方法的语义不能乱用

HTTP方法为你的API操作提供语义含义。错误使用它们会制造混乱并违反可预测性原则。

POST万能论是个陷阱

核心HTTP方法在REST中映射到CRUD操作:

方法 CRUD操作 特性
GET 读取 安全、幂等
POST 创建 非幂等
PUT 替换更新 幂等
PATCH 部分更新 非幂等
DELETE 删除 幂等

一个常见的错误是用POST处理一切。这种反模式强迫开发者猜测你的API如何工作,而不是遵循标准约定:

// 错误:POST万能
POST /api/users/search
POST /api/products/delete/123
POST /api/orders/123/cancel

// 正确:语义清晰
GET    /api/users?name=Smith
DELETE /api/products/123
POST   /api/orders/123/cancellations

第二种方法用GET加查询参数搜索,用DELETE删除资源,并将取消操作建模为新资源的创建。

非CRUD操作怎么设计?

对于非CRUD操作,考虑将它们建模为子资源:

// 不够RESTful
POST /api/invoices/123/sendEmail

// 更RESTful的方式
POST /api/invoices/123/emailDeliveries

这种方法在支持简单CRUD之外的业务操作的同时,保持REST原则。

版本控制:四种策略的权衡

API会演进。没有版本控制策略,你最终会破坏客户端应用。常见的版本控制方法包括:

1. URL路径版本控制

GET /api/v1/users
GET /api/v2/users

优点:最直观,易于调试和测试,浏览器可直接访问
缺点:URL变化可能影响缓存策略,违反"URL表示资源"的纯粹REST理念

这是业界最广泛采用的方式,Twilio、Stripe、GitHub等主流API都采用此方案。

2. HTTP头部版本控制

GET /api/users
Accept: application/vnd.company.api+json;version=1

优点:URL保持干净,更接近REST原则
缺点:不便于浏览器直接测试,调试复杂度增加

Google API设计指南推荐此方式,适合对REST纯粹性有要求的团队。

3. 查询参数版本控制

GET /api/users?version=1

优点:实现简单,向后兼容性好
缺点:容易被忽略,参数可能与业务参数混淆

这通常被认为是反模式,因为它将版本信息与业务逻辑混在一起。

4. 媒体类型版本控制

GET /api/users
Accept: application/json;version=1

优点:完全符合REST原则,支持内容协商
缺点:实现复杂,客户端需要额外处理

无论选择哪种方式,关键是在需要之前就有策略。避免这些版本控制错误:

  • 为每个小改动创建新版本
  • 不记录版本间的变化
  • 没有迁移路径就放弃旧版本
  • 错误使用语义版本号(次版本更新破坏兼容性)

分页设计:Offset还是Cursor?

大多数返回实体列表的端点都需要某种形式的分页。没有分页,一个简单的搜索可能返回数百万条记录,造成不必要的网络流量。

Offset分页:简单但有代价

这是最简单的分页形式:

GET /items?limit=20&offset=100

对应的SQL:

SELECT * FROM items ORDER BY id LIMIT 20 OFFSET 100;

优点

  • 最容易实现,几乎不需要编码
  • 服务器无状态
  • 无论自定义sort_by参数如何都有效

缺点

  • 大偏移值性能差。数据库需要从第0行开始扫描并计数,然后跳过前100000行
  • 新项目插入时不一致(页面漂移)。这在按最新排序时尤为明显
  • 需要COUNT(*)查询获取总数,大表上代价高昂

Cursor分页:一致但复杂

Cursor分页使用前一页的最后一条记录的值作为游标:

GET /items?limit=20
// 返回最后一条记录created_at: 2021-01-20

GET /items?limit=20&created_at=lte:2021-01-20

对应的SQL:

SELECT * FROM items 
WHERE created_at <= '2021-01-20' 
ORDER BY created_at DESC 
LIMIT 20;

优点

  • 与现有过滤器配合工作,无需额外后端逻辑
  • 即使插入新项目也保持一致的排序
  • 大偏移值也能保持一致的性能

缺点

  • 分页机制与过滤和排序紧密耦合
  • 不适用于低基数字段(如枚举字符串)
  • 使用自定义sort_by字段时,客户端逻辑复杂

选择策略

场景 推荐方案
数据量小(<1万条) Offset
数据相对静态 Offset
需要跳页功能 Offset
大数据量高并发 Cursor
实时数据流 Cursor
移动端无限滚动 Cursor

Gusto Embedded API提供了一个优秀的实践:对于相对稳定的数据集(如员工列表)使用offset分页,对于实时数据(如事件流)使用cursor分页。

错误处理:RFC 9457标准

没有什么比无用的错误消息更让API消费者沮丧的了。然而,全面的错误处理往往被视为事后诸葛亮。

为什么HTTP状态码不够用?

HTTP状态码无法总是传达足够的错误信息来提供帮助。虽然人类使用Web浏览器通常可以理解HTML响应内容,但HTTP API的非人类消费者很难做到这一点。

RFC 9457(前身为RFC 7807)定义了一个简单的JSON文档格式来描述遇到的问题的具体细节——“问题详情”(Problem Details)。

标准的Problem Details格式

{
  "type": "https://example.com/probs/out-of-credit",
  "title": "你没有足够的余额",
  "status": 403,
  "detail": "你的当前余额是30,但需要50。",
  "instance": "/account/12345/msgs/abc",
  "balance": 30,
  "accounts": ["/account/12345", "/account/67890"]
}

核心字段

字段 类型 说明
type URI 问题类型的标识符,应该指向人类可读的文档
title string 问题类型的简短人类可读摘要
status number HTTP状态码,仅作参考
detail string 针对此问题具体出现的人类可读解释
instance URI 标识问题具体出现的URI引用

扩展字段:问题类型定义可以扩展问题详情对象,添加特定于该问题类型的额外成员。例如上面的balanceaccounts

错误响应的反模式

// 糟糕:毫无信息
{
  "error": "An error occurred"
}

// 糟糕:总是返回200
HTTP/1.1 200 OK
{
  "success": false,
  "error": "..."
}

第二种方式破坏了客户端期望和依赖状态码的自动化工具。

幂等性:让失败变得安全

网络是不可靠的。在Stripe的基础设施中,我们一直在应对各种网络失败:初始连接可能失败、调用可能在服务器执行操作中途失败、调用可能成功但连接在服务器能够通知客户端之前中断。

幂等性键的工作原理

对于需要"恰好一次"语义的操作(比如收费),幂等性键是解决方案:

POST /v1/charges
Idempotency-Key: 12345
Content-Type: application/json

{
  "amount": 2000,
  "currency": "usd",
  "source": "tok_visa"
}

如果上述Stripe请求因网络连接错误失败,你可以使用相同的幂等性键安全重试,客户只会被收费一次。

三种失败场景的处理

场景 服务端行为
连接失败 第二次请求时服务端第一次看到这个ID,正常处理
操作中途失败 服务端恢复状态,继续执行。如果之前操作已通过ACID数据库回滚,可以安全重试
响应失败(操作成功但客户端未收到结果) 服务端直接返回缓存的成功操作结果

幂等性键的设计原则

  • 键应该是唯一的,通常使用UUID
  • 键应该有合理的过期时间(Stripe使用24小时)
  • 服务端需要存储键与请求状态的映射
  • 并发相同键的请求应该被序列化处理

认证机制的选择指南

API认证确保敏感数据保持安全,通过确认访问API的用户或应用程序的身份。

七种认证机制对比

方法 最佳用例 核心优势 局限性
OAuth 2.0 第三方集成 细粒度访问控制 设置复杂,资源消耗大
API Keys 内部服务或公共API 易于实现 安全性有限,无过期机制
JWT 微服务 无状态,快速性能 无撤销机制,令牌较大
Basic Auth 遗留系统 简单设置 高安全风险,依赖HTTPS
Bearer Auth 现代Web API 基于令牌,可扩展 需要令牌管理
mTLS 高安全系统 证书双向认证 证书管理复杂
OpenID Connect 身份管理 结合认证和授权 学习曲线陡峭

决策框架

高安全需求?→ OAuth 2.0 或 mTLS
需要可扩展性?→ JWT 或 Bearer Auth
追求简单?→ API Keys
需要身份管理?→ OpenID Connect
面向开发者的公共API?→ API Keys

Stripe选择API Keys作为公共API的认证方式,优化了用户体验和入门速度。这证明了认证机制的选择需要权衡安全性与开发者体验。

性能优化的三个维度

HTTP缓存:被忽视的性能利器

HTTP缓存告诉API客户端是否需要反复请求相同数据。正确使用缓存可以显著减少服务器负载和网络延迟。

ETag和Last-Modified

// 第一次请求
GET /api/users/123
Response:
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT

// 后续请求(条件请求)
GET /api/users/123
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

// 如果资源未变化
HTTP/1.1 304 Not Modified

Cache-Control策略

Cache-Control: max-age=3600, public        // 可缓存1小时
Cache-Control: no-cache                     // 每次使用前需验证
Cache-Control: private, max-age=600         // 仅客户端缓存10分钟

速率限制算法选择

算法 特点 适用场景
固定窗口 简单,但窗口边界可能突发 简单限流
滑动窗口 平滑,内存消耗较大 精确限流
令牌桶 允许突发,实现适中 API限流(推荐)
漏桶 恒定速率,不适应突发 流量整形

GitHub的速率限制实现是优秀范例:透明的限制配额和清晰的状态头部显示使用情况。

X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1372700873

响应精简:字段选择

提供字段选择参数,让客户端只获取需要的数据:

GET /api/users/123?fields=id,name,email

// 响应
{
  "id": 123,
  "name": "John Smith",
  "email": "[email protected]"
}

这比返回包含所有字段的"厨房水槽"式响应更高效。每个你包含的字段都有维护成本——你需要在将来支持它。

安全设计的底线

RESTful API设计中的安全错误可能导致数据泄露和合规违规。

常见安全错误

认证不足:在HTTP而非HTTPS上使用基本认证,或未实现令牌过期。所有API都应该强制HTTPS,使用OAuth 2.0或类似的API认证方法。

授权缺失:认证用户但不检查他们是否有权访问特定资源。

信息泄露:在错误消息中返回敏感数据或暴露内部系统细节。Facebook剑桥分析丑闻展示了通过API过度暴露数据可能产生的灾难性后果。

缺少速率限制:没有速率限制使你的API容易受到拒绝服务攻击和滥用。实现速率限制还意味着正确处理和传达超限情况,例如使用HTTP 429响应。

安全检查清单

  • 使用HTTPS,拒绝HTTP请求
  • 实现适当的认证和授权
  • 验证所有输入,无论来源看起来多可信
  • 实施速率限制
  • 在端点和对象两个层面应用授权
  • 只返回必要数据,最小化暴露
  • 记录安全事件用于审计
  • 考虑使用处理常见安全模式的API网关

从反模式中学习

反模式一:RPC风格的API

将方法直接暴露为URL:

// 反模式
GET /api/getUser?id=123
POST /api/createUser
POST /api/deleteUser

// RESTful
GET /api/users/123
POST /api/users
DELETE /api/users/123

反模式二:过度使用POST

POST应该用于创建资源,不是万能动词:

// 反模式
POST /api/users/search          // 应该用GET
POST /api/orders/123/cancel     // 应该建模为资源
POST /api/products/delete/123   // 应该用DELETE

反模式三:忽略HTTP语义

// 反模式:总是返回200
HTTP/1.1 200 OK
{"success": false, "error": "User not found"}

// 正确:使用正确状态码
HTTP/1.1 404 Not Found
{"type": "not-found", "message": "User not found"}

反模式四:泄漏实现细节

// 反模式
GET /api/db/users/table/records
{
  "query": "SELECT * FROM users WHERE id = 123",
  "execution_time_ms": 5
}

// 正确:抽象实现
GET /api/users/123
{
  "id": 123,
  "name": "John"
}

文档:API成功的一半

即使完美设计的API,如果开发者无法弄清楚如何使用它也会失败。

有效API文档包含

  • 清晰的入门指南
  • 带示例的认证说明
  • 所有端点的完整参考
  • 示例请求和响应
  • 错误代码解释
  • 速率限制详情
  • SDK和客户端库
  • 追踪更新的变更日志

OpenAPI规范(前身为Swagger)已成为记录REST API的行业标准。Swagger UI和ReDoc等工具可以从OpenAPI定义生成交互式文档,减少维护高质量文档的工作量。

记住,文档通常是开发者与你的API的第一次交互。它的质量直接影响采用率。


优秀的API设计不是关于知道最先进的技术——而是关于避免摩擦。从外向内设计、关注开发者体验、正确使用HTTP方法、创建直观的URL、构建全面的错误处理、尽早实现版本控制、保持响应专注、让安全成为基础。

最成功的API感觉直观、行为可预测、为开发者解决实际问题。通过遵循这些原则并避免常见反模式,你将创建开发者真正想要使用的API——这是API成功的最终衡量标准。

参考文献

  1. Microsoft. (2025). Web API Design Best Practices - Azure Architecture Center. https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design
  2. Google Cloud. (2025). API Design Guide. https://docs.cloud.google.com/apis/design
  3. Nottingham, M., et al. (2023). RFC 9457: Problem Details for HTTP APIs. IETF. https://www.rfc-editor.org/rfc/rfc9457.html
  4. Stripe. (2017). Designing robust and predictable APIs with idempotency. https://stripe.com/blog/idempotency
  5. Moesif. (2025). REST API Design: Filtering, Sorting, and Pagination. https://www.moesif.com/blog/technical/api-design/REST-API-Design-Filtering-Sorting-and-Pagination/
  6. Zuplo. (2025). Common Mistakes in RESTful API Design. https://zuplo.com/learning-center/common-pitfalls-in-restful-api-design
  7. Gusto. (2025). A Developer’s Guide to API Pagination: Offset vs. Cursor-Based. https://embedded.gusto.com/blog/api-pagination/
  8. Zuplo. (2025). Top 7 API Authentication Methods Compared. https://zuplo.com/learning-center/top-7-api-authentication-methods-compared