《Web API Design The Missing Link》读书笔记


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

4. 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

这是全书非常重要的核心思想之一。

Resource 不只是:

简单属性

还存在:

Resource → Resource

例如:

Dog
 └── owner → Person

最自然的 HTTP 表达是:

{
  "owner": "https://dogtracker.com/persons/98765432"
}

而不是:

{
  "ownerId": "98765432"
}

因为 URL 本身就是可直接使用的导航能力。


例如 ownerId 只能说明:

owner = 98765432

客户端还必须知道:

/persons/{personId}

然后自己构造 URL。

而 Link 直接告诉客户端:

owner → 去这里

因此:

ID
→ 数据

Link
→ 数据 + 如何继续访问

尤其当关系的目标可能有不同 Type 时,Link 更灵活:

Dog.owner
    ↓
Person
或
Institution
或
其他 Resource

不需要:

ownerId
ownerType
personOwnerId
institutionOwnerId

两者用途不同:

Link
→ Navigation
→ "从这里去哪里?"

URI Template
→ Query / Lookup
→ "我知道一些信息,怎么找到目标?"

书中用 Web 类比:

Link
≈ 网页里的超链接

URI Template
≈ Google Search

所以最佳设计不是:

只使用 Links

也不是:

只使用 Templates

而是:

Links + Query Templates

二者互补。


已有 API 可以先:

增加 URL-valued Property

例如:

{
  "ownerId": "98765432",
  "ownerLink": "https://..."
}

ownerLink 可以先只读,由 Server 根据 ownerId 生成。

如果是新 API,则可以进一步直接把关系建模为:

{
  "owner": "https://..."
}

当 Client 通过 Link 导航时,不必事先知道:

这个 URL 对应什么数据库
这个资源属于哪个表
目标 Resource ID 怎么解析

它只需要:

follow link
→ 查看返回的数据
→ 决定下一步

这种方式类似浏览器:

URL → Resource → 进一步 Link

不要轻易把自己的完整 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 找回来。


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。


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

10. 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,而不是完全不同的数据模型。

优先使用 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

21.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

可以根据当前状态提供不同 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 把它遮起来

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 就设计。


如果:

/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。


过去很多人认为:

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

{
  "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

28. 最终记忆版:全书最重要的 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 松耦合为最终目标。


文章作者: Kiba Amor
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 Kiba Amor !
  目录