ebook: Web API Design: The Missing Link
主线:以 Application Developer 为中心,尽量直接使用 HTTP/URI 原生能力,以 Resource/Data Model 为核心,利用 Representation、Link、URL Query 和 HTTP Semantics 构建统一、简单、可预测、低耦合的 Web API。
1. Introduction
核心背景
早期 Web API 往往试图把 CORBA、DCOM 等分布式 RPC 技术搬到 HTTP 上,形成 SOAP/WSDL 等复杂体系。
后来逐渐形成另一种思路:
不要在 HTTP 之上再造一套完整协议
↓
尽量直接使用 HTTP
↓
让 HTTP 本身承担 API 的统一接口
书中的基本立场是:
能直接使用 HTTP,就不要再发明新的 API-specific concept。
为什么这样做?
因为 API Consumer 通常同时使用很多 API,他们已经掌握:
HTTP
URI
Status Code
Header
GET / POST / PUT / PATCH / DELETE
因此:
使用标准
→ 学习成本低
→ 不需要重新学习每个 API 的"私有协议"
→ Client / Server 更松耦合
例如:
POST 创建 Resource
→ 201 Created
→ Location: 新 Resource URL
并发更新
→ ETag + If-Match
多种 Representation
→ Accept
不要用自定义 Header / 自定义状态码替代这些标准机制。
核心结论
Web API 的基础不是”设计一套更好的 RPC”,而是充分利用 Web 已经提供的统一机制。
2. Web APIs and REST
2.1 The Job of the API Designer
核心目标
API Designer 的首要职责:
让 Application Developer 尽可能成功。
所以 API 设计不是从 Server 实现方便不方便出发,而是从:
Client Developer
↓
理解
学习
编码
调试
维护
的总成本出发。
2.2 What is a Web API?
Web API 本质上是:
用于让程序访问 Web Site 的 HTTP Request / Response 模式。
与传统面向人的网站不同,Web API 面向程序消费者。
2.3 What is REST?
书中的观点非常实用:
HTTP = 现实存在的 Web 基础
REST = 影响 HTTP 设计的架构思想
很多所谓 RESTful API 实际是:
HTTP
+
REST 思想
+
一些 RPC / 自定义概念
书中不是 REST Purist,不要求”绝对纯 REST”,而是强调:
在效果相同的情况下,尽可能少增加 HTTP 之外的新概念。
这符合 Occam’s Razor:
用解决问题所需要的最少概念
原因之一是 Loose Coupling:
概念越标准
→ Client 越不依赖 Server 的私有规则
→ Server 越容易演进
3. HTTP and REST:A Data-oriented Design Paradigm
3.1 Data-oriented,而不是 Function-oriented
REST API 关注:
What are the things?
而不是:
What functions can I call?
例如:
Resource-oriented:
/dogs
/dogs/123
/persons/456
/persons/456/dogs
而不是:
/getDog
/createDog
/updateDog
/deleteDog
/getAllDogs
/getRedDogs
为什么 Data-oriented 更好?
因为一旦知道:
/dogs/123
配合 HTTP,你天然已经知道:
GET → 获取
PATCH → 修改部分属性
PUT → 替换
DELETE → 删除
因此 Client 只需要学习:
Resource 的 Data Model
而不是学习:
这个 API 独有的几十个 Function
这就是 REST 的 Uniform Interface Constraint 带来的学习优势。
3.2 API Design Elements
一个 Web API 主要由四部分共同定义:
1. Representation
→ Resource 的字段和关系
2. HTTP Headers
→ 标准 / 必要时自定义
3. URLs / URI Templates
→ 根据数据定位 Resource 的 Query Interface
4. Client-required behaviors
→ Retry
→ DNS Cache
→ Unknown Field tolerance
→ 其他客户端行为约束
关键思想
API 不只是 URL + JSON。
真正完整的 API Contract 是:
Representation
+
HTTP Semantics
+
Headers
+
URL / Query
+
Required Client Behavior
Representation
+
HTTP Semantics
+
Headers
+
URL / Query
+
Required Client Behavior4. Designing Representations
书中特别强调:
先设计 Representation,再设计 URL。
因为 API 最终传输的是 Resource 的 Representation,而不是 Resource 本身。
4.1 Use JSON
推荐使用 JSON,因为:
- 简单
- 易读
- 易映射到主流编程语言
- 已成为 Web API 的事实标准
JSON 的缺点是原生类型较少:
null
boolean
number
string
例如:
Date / Time
URL
需要额外约定,通常通过字符串表达。
4.2 Keep Your JSON Simple
核心规则:
JSON Object 应对应 Data Model 中的 Entity,JSON Property Name 应对应 Entity 的 Property。
推荐:
{
"kind": "Dog",
"name": "Lassie",
"furColor": "brown"
}
而不建议把:
userId
relationship
metadata
等信息塞进 JSON Key,使 JSON 结构本身成为另一套”语法”。
因为:
JSON 越像 Data Model
→ 越容易理解
→ 越容易生成代码
→ 越容易学习
5. Include Links
这是全书非常重要的核心思想之一。
5.1 Relationship 应该使用 Link
Resource 不只是:
简单属性
还存在:
Resource → Resource
例如:
Dog
└── owner → Person
最自然的 HTTP 表达是:
{
"owner": "https://dogtracker.com/persons/98765432"
}
而不是:
{
"ownerId": "98765432"
}
因为 URL 本身就是可直接使用的导航能力。
5.2 为什么 Link 比 ID 更好?
例如 ownerId 只能说明:
owner = 98765432
客户端还必须知道:
/persons/{personId}
然后自己构造 URL。
而 Link 直接告诉客户端:
owner → 去这里
因此:
ID
→ 数据
Link
→ 数据 + 如何继续访问
尤其当关系的目标可能有不同 Type 时,Link 更灵活:
Dog.owner
↓
Person
或
Institution
或
其他 Resource
不需要:
ownerId
ownerType
personOwnerId
institutionOwnerId
5.3 Link 与 URI Template 不是竞争关系
两者用途不同:
Link
→ Navigation
→ "从这里去哪里?"
URI Template
→ Query / Lookup
→ "我知道一些信息,怎么找到目标?"
书中用 Web 类比:
Link
≈ 网页里的超链接
URI Template
≈ Google Search
所以最佳设计不是:
只使用 Links
也不是:
只使用 Templates
而是:
Links + Query Templates
二者互补。
5.4 Links 可以逐步演进
已有 API 可以先:
增加 URL-valued Property
例如:
{
"ownerId": "98765432",
"ownerLink": "https://..."
}
ownerLink 可以先只读,由 Server 根据 ownerId 生成。
如果是新 API,则可以进一步直接把关系建模为:
{
"owner": "https://..."
}
5.5 Link 的重要副作用:更接近 Web 的通用导航模型
当 Client 通过 Link 导航时,不必事先知道:
这个 URL 对应什么数据库
这个资源属于哪个表
目标 Resource ID 怎么解析
它只需要:
follow link
→ 查看返回的数据
→ 决定下一步
这种方式类似浏览器:
URL → Resource → 进一步 Link
5.6 Link 的 Server-side 注意事项
不要轻易把自己的完整 Absolute URL 存进数据库。
例如:
https://api.example.com/person/123
因为:
生产环境
Integration
Test
Pre-production
不同 Domain
可能发生变化。
更合理:
DB:
/person/123
Output:
https://api.example.com/person/123
外部系统的绝对 URL 则通常需要完整保存。
6. Designing URLs
6.1 In URLs, nouns are good; verbs are bad
核心规则:
URL 识别 Thing,不识别 Action。
推荐:
/dogs
/dogs/123
/persons
/persons/123
避免:
/getDog
/createDog
/deleteDog
/startProcess
原因:
Verb → RPC 思维
Noun → Resource 思维
而 HTTP Method 已经负责操作语义。
6.2 Well-known URLs
API 至少要有一个客户端可以知道的起点,例如:
/
或者:
/dogs
/persons
Root Resource 可以进一步暴露:
dogs → /dogs
persons → /persons
这样整个 API 更容易:
Discover
Crawl
Navigate
7. Designing Entity URLs
7.1 Client 不应该依赖 URL 拼接规则
书中一个很重要但经常被忽略的观点:
Client 不应该被迫根据 Resource ID 自己构造 Entity URL。
创建 Resource 时:
POST /dogs
↓
201 Created
↓
Location: /dogs/123
Server 直接告诉 Client URL。
以后 Client 可以:
保存 URL
Bookmark
Link
如果忘记 URL,可以通过 Collection Query 找回来。
7.2 Permalink
Persistent Link 需要稳定。
因此:
用于 Link 的 URL
→ 不应该把可能变化的业务信息放进 Identity
例如:
UUID
通常比:
name
status
category
更适合成为永久标识。
7.3 The Web is Flat
现实世界可能是:
Person
└── Dogs
└── Medical Records
但这不意味着 Persistent URL 必须严格模拟这个层级。
例如:
/dogs/UUID
/persons/UUID
完全可以是平坦的。
层级更适合 Query / Navigation,而不是强行作为永久 Identity。
7.4 Permalink 中的 Type
Permalink 不只是:
UUID
还可能需要:
/type/UUID
原因不只是告诉 Client:
“这是 Dog。”
更重要的是让 Server 可以根据 Type 将 URL 路由到正确的实现单元:
URL
↓
Type
↓
Handler / Implementation Unit
因此 Type 本身应该足够稳定,以支持长期的实现映射。
8. Solutions to the Renaming Dilemma
问题
name
→ Human-friendly
→ 但可能改变
如果:
/person/Joe
直接成为 Persistent Link:
Joe → Smith
就会破坏旧链接。
解决方案
将:
Stable Identity
与:
Human-friendly Query
分开。
Permalink
/person/UUID
↓
Persistent Link
Query URL
/person/{name}
↓
Human-friendly Lookup
因此:
Identity ≠ Name
如果还需要旧名称永久兼容,则可以采用:
Alias
+
Redirect
代价是维护历史名称以及名称复用问题。
9. Designing Query URLs
核心思想
URI Template 实际上是在定义:
这个 API 自己的 Query Language。
因此问题不是:
URL 长不长
而是:
是否 Regular?
是否 Predictable?
是否与 Data Model 一致?
目标是:
理解了 Data Model 的 Client,应该能大致预测 Query URL。
9.1 Relationship Traversal
例如:
/persons/5678/dogs
可以解释成:
Root
↓ persons
↓ select 5678
↓ dogs
更复杂:
/dogs/123456/owner/spouse
本质是:
Dog
↓ owner
Person
↓ spouse
Person
因此 Path 可以自然表示 Resource Graph Traversal。
9.2 URL 与 Representation 应该对称
如果 Representation 中存在:
{
"dogs": "https://.../persons/5678/dogs"
}
那么 Query URL 最好也允许:
/persons/5678/dogs
反之:
如果 Query URL 表达了一条关系
→ Representation 也应该能表达这条关系
形成:
URL Relationship
↕
Representation Relationship
这样 API 更容易学习。
9.3 一般化 Query URL 模型
可以抽象为:
/{relationship-name}
[/{resource-id}]
...
/{relationship-name}
[/{resource-id}]
其中:
单值关系
→ 直接进入下一层
多值关系
→ 需要 Resource ID
这是一种统一的 Graph Traversal Model。
9.4 Path Parameter / Matrix Parameter
书中也讨论了 Matrix Parameter 风格。
重点不是哪一种语法绝对正确,而是:
选择一种 Client 容易理解、实现一致、可预测的 Query URL 风格。
9.5 Filtering Collections
Collection 可以通过 Query Parameter 过滤:
/dogs?color=red
/dogs?state=running
这里应该区分:
Path
→ Relationship / Traversal
Query Parameter
→ Filtering / Selection
Path
→ Relationship / Traversal
Query Parameter
→ Filtering / Selection10. Responses That Don’t Involve Persistent Resources
问题
有些 API Response 是:
计算结果
转换结果
临时结果
例如:
Currency Conversion
容易设计成:
/convertCurrency
书中的思想
不要因为结果不是持久化数据库实体,就直接转成 RPC。
先问:
Response 表示什么”东西”?
如果是:
Monetary Amount
就可以:
/monetary-amount/100/EUR/CNY
或者:
GET /monetary-amount/100/EUR
Accept-Currency: CNY
即:
Algorithm
→ Server 内部实现
Result
→ Resource
POST 也可以
例如:
POST /currency-converter
但有两个问题:
1. Response 无法直接使用标准 HTTP Cache
2. 容易逐步变成 RPC
如果你开始为 POST Response 自己定义:
cacheKey =
amount + inputCurrency + outputCurrency
这个 Key 实际上已经变成了一个”伪 URL / Identity”。
因此书中建议:
如果 Response 可以自然地定义成 Noun Resource,优先考虑 GET。
11. More on Representation Design
11.1 self 与 kind
推荐 Resource Representation 至少包含:
{
"self": "https://example.com/dogs/123",
"kind": "Dog"
}
含义:
self
→ 我是谁?
kind
→ 我是什么?
这样 Resource 即使脱离上下文,也能够自描述。
11.2 为什么 self 很重要?
嵌套对象:
{
"owner": {
...
}
}
如果没有 self:
这个 object 到底对应哪个 Resource?
可能只能从父对象推断。
有:
self
就能独立识别。
11.3 为什么 kind 很重要?
除了描述类型:
Dog
Person
Collection
Page
还可以帮助 Client:
识别 Resource Type
处理未知 Resource
从而提高 Evolution 能力。
12. How Should I Represent Collections?
核心原则
Collection 本身也是 Resource。
推荐:
{
"self": "https://example.com/dogs",
"kind": "Collection",
"contents": [
{
"self": ".../dogs/123",
"kind": "Dog",
...
}
]
}
这样:
Collection
= 普通 Resource
而不是特殊的数据结构。
为什么保留外层 Object?
三个优势:
1. Collection 与其他 Resource 一致
2. Collection 自描述
3. 给 Collection 自己的 Metadata 预留位置
例如:
creationDate
owner
modificationDate
revisionID
parentResource
如果 Client 不需要元素数据,也可以简单返回:
[URL, URL, URL]
12.1 不需要专门的 Collection Media Type
书中不倾向:
application/vnd.xxx-collection+json
因为:
JSON 本身没有变化
变化的是 Resource Semantics
因此更合理的是:
普通 JSON
+
Collection Resource Type
而不是:
新的 Media Type
13. Paginated Collections
大 Collection 不适合一次返回:
Server 压力 ↑
Client 内存 ↑
Network ↑
因此:
Collection
↓
Page 1
Page 2
Page 3
...
Page 本身也是 Resource
例如:
{
"self": ".../dogs?limit=25&offset=0",
"kind": "Page",
"pageOf": ".../dogs",
"next": ".../dogs?limit=25&offset=25",
"contents": [...]
}
核心字段:
self
→ 当前 Page
kind
→ Page
pageOf
→ 属于哪个 Collection
next
→ 下一页
previous
→ 上一页
contents
→ 当前 Page
一个完整 Collection 怎么定义?
Collection
=
Page1.contents
+
Page2.contents
+
Page3.contents
+ ...
因此 Page 是完整 Collection 的一种分页 Representation,而不是完全不同的数据模型。
使用标准 Link Relation
优先使用 IANA 已注册的:
next
previous
first
last
不要无意义地重新发明:
nextPage
previousPage
只有真正属于自己业务领域的关系才需要自定义名字。
14. Custom Resource Types
如果资源类型需要跨团队、跨 API 全局区分,可以使用 URL 作为 Type Identifier:
kind:
"https://example.com/types#Dog"
优势:
避免 Type Name Collision
缺点:
JSON 更长
更复杂
因此不是必须,而是在需要 Global Type Identity 时使用。
15. Supporting Multiple Formats
默认:
JSON
如果需要:
XML
HTML
其他 Representation
应该使用:
Accept: application/json
Accept: application/xml
而不是:
/dogs.json
/dogs.xml
也就是说:
Resource Identity
→ URL
Representation Format
→ HTTP Content Negotiation
15.1 PATCH 输入格式
书中强调:
应该使用 PATCH。
常见格式:
JSON Merge Patch
→ 简单
→ 易实现
→ 表达能力较弱
JSON Patch
→ 更复杂
→ 表达能力强
两者都可以支持。
尤其 PATCH 对 Evolution 很重要:
PUT
→ 整体替换
→ 新字段容易被旧 Client 丢掉
PATCH
→ 只修改明确指定的字段
→ 更安全
16. Property Names
书中没有认为:
camelCase
vs
snake_case
存在绝对答案。
原则是:
从 Client Developer 的语言生态选择。
通常:
JavaScript / Java / Objective-C
→ camelCase
Python / Ruby
→ snake_case
对于一般 Web API,书中倾向:
camelCase
同时避免
特殊字符:
/ \ = : ; , . ? # < > @ -
尤其 - 在 JavaScript 等语言中不是普通 Identifier 字符。
同时避免与语言保留字 / Global 冲突:
this
window
document
class
function
...
17. Date and Time Formats
不同 API 常见:
ISO / XML Schema DateTime
Unix Timestamp
自定义字符串
书中倾向使用标准格式,而不是自定义格式。
常见选择:
XML Schema DateTime
它是 ISO 8601 的一个子集。
Unix Timestamp 也有其优势:
简单
数字
与时区无关
方便做时间差
重点:
选择标准格式,并在整个 API 中保持一致。
18. Chatty APIs
18.1 “REST 一定 Chatty”是误解
书中的观点非常明确:
如果 API 很 Chatty,问题通常不是 REST,而是 Resource Design 错了。
常见错误是:
API = 完全按照数据库 Third Normal Form 建模
然后 UI 为展示一张页面:
GET order
GET line items
GET customer
GET account
GET ...
请求数量爆炸。
18.2 Normalized + Denormalized Resources 可以共存
可以同时拥有:
Normalized Resources
→ 更适合 Update / Domain Model
Denormalized Read-only Resources
→ 更适合 UI / Read Performance
关键认知:
Denormalized Resource 仍然是真正的 REST Resource。
只要它:
有意义
有 URI
有自己的 Resource Semantics
就完全可以存在。
19. Pagination and Partial Response
19.1 Partial Response
客户端经常不需要完整 Resource:
100 个字段
→ 实际只需要 5 个
因此可以提供:
?fields=id,name,color
好处:
Payload ↓
Bandwidth ↓
Parsing ↓
特别适合:
Mobile
高延迟网络
复杂嵌套 Resource
而且可以进一步选择相关 Resource,减少额外请求。
19.2 显式 Pagination 参数
除了:
next
previous
first
last
有时还希望 Client 直接控制分页。
典型模型:
Facebook
offset + limit
Twitter
page + rpp
LinkedIn
start + count
书中偏好:
limit + offset
但最终数据库排序方式和底层 DB 技术可能迫使实现做出调整。
20. Handling Errors
核心观点
Error 不是异常分支,而是:
API Developer Experience 的核心组成。
因为 Client 看到 Server 就像:
Black Box
因此 Error 是 Client 理解:
发生了什么
为什么
应该怎么做
的重要工具。
20.1 使用标准 HTTP Status Code
不要把 Status Code 当作:
function return code
而应该把整个 Response 看成:
Status
+
Headers
+
Body
例如:
201 Created
+
Location
表示:
Resource 创建成功
+
告诉 Client 新 Resource 的 URL
又例如:
405 Method Not Allowed
+
Allow: GET, DELETE, PATCH
告诉 Client:
这个 Resource 支持哪些 Method
20.2 Error Payload 尽量 Verbose
推荐:
{
"developerMessage": "...",
"userMessage": "...",
"errorCode": 12345,
"moreInfo": "https://..."
}
含义:
developerMessage
→ 开发者理解问题
userMessage
→ 可以传递给最终用户
errorCode
→ 程序化识别
moreInfo
→ 进一步文档
核心原则:
Verbose + Plain Language + Actionable Hints + More Info Link。
21. Modeling Actions
当需求是:
start process
stop process
pause process
resume process
最容易退化成:
/start
/stop
/pause
但书中提出两种 Resource-oriented 做法。
21.1 把 Action 建模成 State Change
例如:
state:
initial
started
paused
stopped
操作本质变成:
修改 Resource State
而不是:
调用 Verb Endpoint
调用 Verb Endpoint21.2 把 Action 建模成 Action Request Resource
例如:
POST /processes/123/requests
提交:
StartRequest
StopRequest
PauseRequest
ResumeRequest
这样 Action 本身就是 Resource。
特别适合:
需要记录:
Who / What / When
需要异步处理
需要跟踪执行进度
因为:
POST
→ 创建 Request Resource
后续
→ GET Request
→ 查询 State / Progress
POST
→ 创建 Request Resource
后续
→ GET Request
→ 查询 State / Progress21.3 Link Presence 表示 Capability
可以根据当前状态提供不同 Link:
state = initial
→ startRequests
state = running
→ pauseRequests
→ stopRequests
state = paused
→ resumeRequests
→ stopRequests
因此:
Link 存在
→ Action 当前可用
Link 不存在
→ Action 当前不可用
即:
Representation 不仅描述 State,也可以表达 Capability。
22. Authentication
书中的结论非常直接:
使用 OAuth 2.0。
优势:
Client 不需要共享用户密码
并且可以:
Revoke User Token
Revoke App Token
而不必修改用户原始密码。
特别适合:
Mobile Device 被盗
Rogue App
Token 泄露
23. Complement with an SDK
SDK 很有价值,但:
SDK 不能替代优秀的 Web API Design。
因为 SDK 的质量直接取决于底层 Web API:
好的 Web API
→ SDK 更简单
→ SDK 更可靠
→ 更容易支持更多语言
而且即使使用 SDK,开发者仍然可能在:
Debugging
Network Inspection
Performance Tuning
时直接看到底层 HTTP。
因此:
Web API
↓
良好设计 + 文档 + Samples
↓
SDK
而不是:
API 设计不好
→ 用 SDK 把它遮起来
API 设计不好
→ 用 SDK 把它遮起来24. Versioning
这是书中最后一个重要争议点。
24.1 首选:Backward-compatible Evolution
并不是每次 API 变化都需要:
v1 → v2
很多变化可以兼容:
新增 Property
新增 Resource Type
前提是 Client:
能够忽略未知字段
书中特别建议可以在服务端加入一些额外字段,提前验证 Client 是否具备这种容错能力。
24.2 PATCH 比 PUT 更有利于 Evolution
原因:
PUT
→ 完整替换
→ Client 必须发送所有字段
PATCH
→ 只修改指定字段
→ Server 负责 Merge
假设 Server 后来增加:
newProperty
旧 Client 使用 PUT 时可能把它覆盖掉。
PATCH 则不会。
因此:
PATCH 不只是更新语义问题,也是 API Evolution Strategy。
24.3 New Resource Types 通常也可以兼容
可以新增:
Dog
Person
Veterinarian
但需要确保旧 Client:
不会因为未知 Resource Type 出现就崩溃
因此:
Unknown Type tolerance
也是 API Evolution 的重要 Client Requirement。
24.4 什么时候才需要新 Version?
当变化:
无法 Backward Compatible
+
已经是 Fundamental Data Model Change
才值得:
New API
+
Migration
书中甚至认为很多人所谓:
"介于兼容与完全新 API 之间"的变化
在实际 API 中可能并不存在。
24.5 不做 Versioning 也是一种合理选择
书中明确提出:
Doing Nothing for Versioning 是一种合理的 API Strategy。
如果:
没有版本号
以后再加入也可以:
无 Version Identifier
→ 默认 V1
因此:
Versioning 不一定要 Day 1 就设计。
24.6 Version in URL 与 Link 冲突
如果:
/v2/dogs/123
然后 Dog:
{
"owner": "https://.../v2/persons/987"
}
Server 实际上在猜:
Dog V2
→ Client 是否真的需要 Person V2?
问题在于:
Lassie 是 Dog
它被 Person 拥有
而不是被"Person V2"拥有
也就是说:
Version 是 Representation/API Interface 的概念,不天然属于 Domain Resource Identity。
因此:
Version in URL
+
Links
天然比较别扭。
24.7 如果必须 Version,Header 更自然
例如:
Accept-Version: 2
相比:
/v2/...
更适合与 Links 一起使用。
也可以采用:
Canonical Resource URL
+
Version-specific URL
或 Relative URL 等复杂方案,但都会增加 Client 的额外知识。
最终复杂度排序大致是:
No Versioning
↓
Header Versioning
↓
URL Versioning + Links
越往下,需要 Client 学习的特殊规则越多。
25. Conclusion
书中最终把整个方法论总结成几个核心思想。
25.1 HTTP / URI First
尽量只使用:
HTTP
URI
HTTP Headers
HTTP Methods
HTTP Status
这样:
Client 学习成本 ↓
API-specific Concepts ↓
Client / Server Coupling ↓
25.2 Entity-oriented
Web API 更像:
Database
而不是:
Programming Language API
即:
定义 Data Model
+
让 HTTP 提供统一操作
而不是:
定义大量 Function Endpoint
25.3 Representation 是 API Design 的核心工作
应该重点设计:
JSON
Properties
Links
Collections
Pages
Types
Dates
Formats
因为 HTTP 已经提供了基本 CRUD,真正需要 API Designer 大量设计的是:
Data 如何表示,以及 Data 如何查询。
25.4 Query URL 是 API 自己的 Query Language
HTTP 本身解决:
CRUD
但没有定义:
如何查询你的 Domain Model
因此 Query URL 的设计非常重要。
目标:
Regular
Predictable
Data-model-driven
让 Client:
学会你的 Data Model 后,就能大致预测 Query URL。
25.5 Links 不只是给 Generic Client
过去很多人认为:
Hypermedia
→ 只对"像 Browser 一样"的 Generic Client 有价值
书中后来越来越明确地认为:
Links
→ 对所有类型的 Client 都有价值
因为它直接降低:
URL 构造成本
+
Relationship 学习成本
+
API Documentation 依赖
26. Appendix:Other Approaches to Representing Links
书中最后讨论几种更复杂的 Link 表示方式。
26.1 Simple href
{
"owner": {
"href": "https://..."
}
}
优点:
简单
可以知道这是 URL
可以扩展 Link Metadata
简单
可以知道这是 URL
可以扩展 Link Metadata26.2 links + href + rel
{
"links": [
{
"href": "https://...",
"rel": "owner"
}
]
}
优点:
Generic Client 更容易识别 Link
缺点:
比简单 JSON 更复杂
26.3 Typed Value
例如:
{
"owner": {
"dataType": "URI",
"value": "https://..."
}
}
优点:
明确声明类型
问题:
JSON 复杂度明显提高
作者自己的实践最终又倾向于:
简单 String
因为过度类型化容易与开发者的自然编程习惯产生摩擦。
26.4 其他 Hypermedia 规范
书中提到:
Siren
HAL
JSON-LD
RDF/JSON
Collection+JSON
Hydra
它们都提供了更多能力,但代价是:
更多概念
更多规则
更高学习成本
因此全书仍然坚持:
只使用真正解决问题所需要的复杂度。
27. 全书知识结构
可以把整本书压缩成下面这条知识链:
API Design
│
▼
Application Developer
First Priority
│
▼
Use HTTP Directly
│
▼
Data-oriented / REST
│
┌───────────┴───────────┐
▼ ▼
Resource Model HTTP Semantics
│ │
│ GET / POST /
│ PUT / PATCH /
│ DELETE
│
▼
Representation
│
┌───────┼────────┐
▼ ▼ ▼
JSON self kind
│
▼
Links
│
┌─────┴─────┐
▼ ▼
Navigation Query
│ │
Link URI Template
│
▼
Query URLs
│
Relationship Traversal
│
Filtering
│
▼
Entity / Collection
│
┌─────┴─────┐
▼ ▼
Collection Page
│
▼
Large Dataset
│
Pagination +
Partial Response
│
▼
Client Performance
│
▼
Special Problems
│
┌───────┼───────────────┐
▼ ▼ ▼
Rename Action Non-persistent
│ Response
▼
State / Request
│
▼
Production API
│
┌───────┼────────────┐
▼ ▼ ▼
Errors OAuth2 SDK
│
▼
Evolution
│
▼
Compatibility
│
▼
Versioning
API Design
│
▼
Application Developer
First Priority
│
▼
Use HTTP Directly
│
▼
Data-oriented / REST
│
┌───────────┴───────────┐
▼ ▼
Resource Model HTTP Semantics
│ │
│ GET / POST /
│ PUT / PATCH /
│ DELETE
│
▼
Representation
│
┌───────┼────────┐
▼ ▼ ▼
JSON self kind
│
▼
Links
│
┌─────┴─────┐
▼ ▼
Navigation Query
│ │
Link URI Template
│
▼
Query URLs
│
Relationship Traversal
│
Filtering
│
▼
Entity / Collection
│
┌─────┴─────┐
▼ ▼
Collection Page
│
▼
Large Dataset
│
Pagination +
Partial Response
│
▼
Client Performance
│
▼
Special Problems
│
┌───────┼───────────────┐
▼ ▼ ▼
Rename Action Non-persistent
│ Response
▼
State / Request
│
▼
Production API
│
┌───────┼────────────┐
▼ ▼ ▼
Errors OAuth2 SDK
│
▼
Evolution
│
▼
Compatibility
│
▼
Versioning28. 最终记忆版:全书最重要的 15 条原则
1. API 的第一目标是 Application Developer Productivity。
2. 尽量直接使用 HTTP,不要重新发明一套 RPC 协议。
3. API 应围绕 Resource / Data Model,而不是 Function 设计。
4. URL 标识 Thing,因此使用 Noun,而不是 Verb。
5. Representation 是 API Design 的核心,应先设计 Representation 再设计 URL。
6. JSON 越接近 Data Model 越好:
Property → Property
Object → Entity
7. Resource Relationship 用 Link 表达,而不仅仅是 ID。
8. Link 与 URI Template 是互补关系:
Link = Navigation
Template = Query / Lookup
9. Persistent Link 使用稳定 Identity;
Human-friendly Name 适合作为 Query Key,而不是永久 Identity。
10. Query URL 应该是 Data Model 的可预测 Query Language。
11. Collection 和 Page 都是 Resource,不要把它们当特殊 JSON。
12. REST 不要求 API 完全规范化;
可以增加 Denormalized Read Resources 解决 Chatty 问题。
13. Action 优先建模为:
State Change
或
Action Request Resource。
14. API Evolution 优先于 Versioning:
能兼容就不要升级版本。
15. 复杂度越低越好:
Use the simplest design that preserves
consistency, predictability, and loose coupling。
这些原则基本覆盖了书从 Introduction → REST → Representation → Links → URLs → Query → Collections → Pagination → Errors → Actions → Authentication → SDK → Versioning → Conclusion 的完整论证主线。
29. 一句话理解整本书
不要把 Web API 当成”通过 HTTP 暴露的一组函数”,而应该把它设计成”一个可通过 HTTP 统一访问的数据模型”:Resource 定义数据,Representation 定义数据如何呈现,Links 定义资源之间如何导航,Query URLs 定义如何查找资源,而 HTTP 本身提供统一的 CRUD、缓存、并发控制、状态码和内容协商机制;整个设计始终以降低 Client 学习成本和保持 Client/Server 松耦合为最终目标。