2. RESTful API 设计原则:REST 架构风格与资源导向设计

说实话,我刚开始学REST时,以为它就是个URL命名规范——把动词换成名词、用个斜杠就行了。后来自己写了一套给令牌化服务用的接口,上线第二天就被后端同事找上门,说“你这接口用起来跟RPC有啥区别?” 嗯,从那以后我才认真去读Fielding的,才明白REST是一组架构约束,不是模板。今天我们就聊聊这组约束,以及“资源导向设计”到底怎么落地。

2.1 什么是REST?

REST(Representational State Transfer)是Roy Fielding博士在2000年提出的架构风格。它定义了六项约束,只有全部满足才能称为RESTful。说白了,它就是一套让分布式系统更松耦合、更可缓存的规则。

  • 客户端-服务器:分离关注点,客户端不关心数据存储,服务器不关心UI状态。
  • 无状态:每个请求包含所有必要信息,服务器不保存客户端上下文。我踩过这个坑,刚开始把用户会话塞到服务端内存里,结果扩缩容时session全丢了。
  • 缓存:响应可被隐式或显式标记为可缓存,减少网络开销。
  • 统一接口:这是REST的核心特征,包括资源标识、通过表示操作资源、自描述消息、HATEOAS(超媒体作为应用状态引擎)。
  • 分层系统:中间层(如反向代理、缓存)对客户端透明。
  • 按需代码(可选):服务器可以临时向客户端传递脚本,比如小程序逻辑。
统一接口是REST的灵魂。它让组件之间的交互基于标准语义,而不是定制化接口。你在令牌化服务中设计/tokens时,用GET获取列表、POST创建新令牌,这就是在利用统一接口。

2.2 资源导向设计

资源导向(Resource-Oriented)就是把所有业务实体都抽象为“资源”。每个资源有一个唯一的URI,通过HTTP方法来表达操作意图。你想想看,如果资源是“订单”,那URI就是/orders/{id},获取用GET,创建用POST,替换用PUT,部分更新用PATCH,删除用DELETE。

我建议大家遵循几条命名惯例:

  • 使用名词复数:/tokens 而不是 /getToken
  • 路径层级表达从属关系:/customers/{id}/cards 表示某个客户下的卡片
  • 查询参数用来过滤、分页:?status=active&page=2
  • 避免将动作塞进URI:POST /tokens + body 中的 action: detokenize 好于 /detokenize
我个人习惯在URI中只出现无状态的操作。比如令牌化服务的核心操作“令牌化”和“反令牌化”,我设计为:
POST /tokens/tokenize 和 POST /tokens/detokenize? 不对——这又回到RPC了。更REST的做法是:
POST /tokens(创建令牌)和 POST /tokens/{}/detokenize(对特定令牌执行反令牌化,把反令牌化看作一种动作资源)。不过实践中很多团队为了清晰,还是会用自定义action。关键是你得知道理想态是什么。

2.3 传统RPC与REST的对比

很多人在设计API时不自觉就写成RPC风格。下面这张表能帮你快速区分:

维度 RPC风格 REST风格
URI设计 动作路径,如 /getUser?id=1 资源路径,如 /users/1
HTTP方法 通常只用GET或POST 使用全部语义(GET/POST/PUT/PATCH/DELETE)
状态管理 依赖服务端会话 无状态,每个请求自包含
缓存能力 弱,因为相同URL可能返回不同数据 强,可充分利用HTTP缓存机制
耦合性 客户端与服务端绑定接口签名 客户端通过媒体类型和链接解耦
典型示例 POST /api?method=createToken POST /tokens(创建令牌资源)

记住一点:REST不是HTTP的CRUD,而是通过资源的表述来驱动状态转移。你发出一个GET请求收到JSON,JSON里可能包含超链接(HATEOAS),客户端跟着链接走到下一个状态——这才是REST想表达的东西。

2.4 令牌化服务中的资源设计实例

我们拿令牌化服务举例。假设业务需要:生成令牌、通过令牌获取明文(反令牌化)、查询令牌状态、删除令牌。

  • 令牌集合:GET /v1/tokens → 获取令牌列表(分页)
  • 创建令牌:POST /v1/tokens → body携带明文数据,返回令牌
  • 获取单个令牌:GET /v1/tokens/{id} → 但注意,出于安全通常不允许直接获取明文,所以返回的表示只包含元数据,不包含明文。反令牌化应该走单独接口
  • 反令牌化:POST /v1/tokens/{id}/detokenize → 看作一个动作资源,body传入权限凭证,返回明文
  • 删除令牌:DELETE /v1/tokens/{id}
我曾经在设计第一个版本时,把反令牌化写成了POST /v1/detokenize,请求体里同时带令牌ID和明文。结果上线后运维投诉:日志里全是不相关的请求,无法通过URL做权限控制。后来改成POST /v1/tokens/{id}/detokenize,URL就能精确匹配到某个令牌,权限模型也清晰了。这就是资源导向设计的好处——路径本身就是业务语义。

下面这张图展示了REST架构的核心概念如何在令牌化服务中流转:

REST 架构核心概念在令牌化服务中的流转 客户端 HTTP 请求/响应 资源标识符 URI 资源表示 (JSON/XML) 状态转移 (HATEOAS 链接) 资源状态 (令牌数据) 核心原则 1. 资源通过URI标识 2. 使用标准HTTP方法 3. 表示驱动状态转移 4. 无状态、可缓存 5. 超媒体作为引擎 以令牌化服务为例

看这张图,客户端发出一个HTTP请求指向/v1/tokens/abc123(URI),服务端返回一个JSON表示。这个JSON里可能包含_links字段,指向可执行的操作(如“反令牌化”的URL)。客户端通过这个链接拿到明文,这就是一次状态转移。整个过程服务器不保存客户端会话,所有状态都存在于资源和表示中。

一句话总结REST的资源导向设计:把业务实体映射为资源,用HTTP标准方法表达操作,通过超媒体驱动客户端前进,保持无状态以便横向扩展。你在设计令牌化接口时,先问自己:“这个动作能不能拆成一个资源 + 一个HTTP方法?” 能,就用REST;不能,考虑是不是需要RPC,但至少你知道自己放弃了什么。
一个小练习:试试将“批量生成令牌”设计成RESTful。你不能用POST /batch-create-tokens。正确的思路是:POST /tokens/batch? 或者用资源集合的子资源。我倾向于POST /tokens/batch,把batch看作一个动作资源。但更纯粹的做法是用POST /tokens并允许body传入数组。你觉得哪种更适合你的团队? 嗯,没有标准答案,但理解原则后你可以做出有依据的选择。