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引用 |
扩展字段:问题类型定义可以扩展问题详情对象,添加特定于该问题类型的额外成员。例如上面的balance和accounts。
错误响应的反模式
// 糟糕:毫无信息
{
"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成功的最终衡量标准。
参考文献
- Microsoft. (2025). Web API Design Best Practices - Azure Architecture Center. https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design
- Google Cloud. (2025). API Design Guide. https://docs.cloud.google.com/apis/design
- Nottingham, M., et al. (2023). RFC 9457: Problem Details for HTTP APIs. IETF. https://www.rfc-editor.org/rfc/rfc9457.html
- Stripe. (2017). Designing robust and predictable APIs with idempotency. https://stripe.com/blog/idempotency
- Moesif. (2025). REST API Design: Filtering, Sorting, and Pagination. https://www.moesif.com/blog/technical/api-design/REST-API-Design-Filtering-Sorting-and-Pagination/
- Zuplo. (2025). Common Mistakes in RESTful API Design. https://zuplo.com/learning-center/common-pitfalls-in-restful-api-design
- Gusto. (2025). A Developer’s Guide to API Pagination: Offset vs. Cursor-Based. https://embedded.gusto.com/blog/api-pagination/
- Zuplo. (2025). Top 7 API Authentication Methods Compared. https://zuplo.com/learning-center/top-7-api-authentication-methods-compared