系统设计 | RESTful API 使用问题和建议
文 | 少个分号 (转载请注明出处)
公众号:DDD 和微服务
知乎:少个分号
微信号:shaogefenhao
网站:shaogefenhao.com
虽然 RESTful API 已经成为业界对于 API 的共识,但是不得不说,但是不得不说它具有很多局限性。
RESTful API 和很多的技术流传的原因类似:始于一种非常理想化的愿景,但是在落地时却需要做出权衡和取舍。
它的流行开始于 Roy Fielding 的演讲,Roy Fielding 也是 HTTP 协议标准作者之一。
我猜测 Roy Fielding 的想法是,HTTP 协议已经是一个完善的应用层协议了,对于应用开发来说,只需要将所有的网络数据抽象为 URI 资源,然后配合可选的 Method 以及状态码就够用了。
但是,在我们实际落地的情况中,HTTP 的表达力完全不够。其限制主要有这几个:
不是所有的信息都能抽象为静态资源,总会有一些动态行为存在。例如 Github 的 Star 操作,即使抽象为 star-records 也违反人的表达认知。
HTTP Method 无法在应用层随意拓展,很多行为难以被表达为合适的 Method
HTTP Status 无法在应用层拓展,很多业务状态无法使用 HTTP Status 表达
基于这些原因,人们为了遵守 RESFul 不得不绞尽脑汁,而如果使用 RPC 风格的 API 只需要使用合适的动词作为 URI 以及合适的 Payload 报文格式即可。
但是,总体来说,在一定程度上,使用 RESTful 风格,可以做到自解释性,减少了文档的依赖。而它的缺点,也可以通过一些团队规约避免。
本文基于技术研讨会讨论的内容,整理了一些使用 RESTful 的一些建议,包括如何通过团队契约在一定程度上弥补表达力不够的问题。
01 使用版本号解决版本不兼容问题
版本化 API 会有很多好处,而版本化 API 有很多种风格,包括:
使用 URL 前缀
使用 URL 后缀
使用 Header 传参
使用 Query 参数
推荐使用 URL 前缀实现,这样对后面 API 定义无侵入性且能通过 URL 表达版本。
实例:GET /v1/products/{id}
02 资源路径参考领域模型
在一般情况下,可以以模块、聚合根作为路径前缀,让路径排列更有规律。
参考类似模式: /[模块]/[版本号]/[聚合根]/{id}/[实体]/{id}/[属性/动作]
在微服务项目中,模块部分可以是服务名
资源参考领域模型设计用词,因此需要使用名词复数
为了弥补 RESTful 不足,允许在路径结尾使用属性或者动作满足特定业务需求
一般 URL 路径和文件名风格类似,使用中横线(Dash)
和领域模型的层次类似,URL 层次不要太深,通过拆分小聚合实现短 URL
03 谨慎选择 HTTP Method
HTTP 协议提供了很多的 Method,但是处于团队理解成本的原因建议只使用下面几个 HTTP Method:
GET 查询
POST 新增
PUT 修改/更新
DELETE 删除
避免使用 PATCH,原因是无法表达业务含义,往往产生破坏性变更。例如,将保存的出库单提交,业务上需要修改单据的状态。应该避免使用 Patch 部分更新单据,建议使用 PUT /xxx/submit
的形式设计,让团队更容易理解,保证业务一致性。
04 实体的单复数应具有实际意义
通过 URL 应该能识别出返回的结构类型是否是一个列表或者分页的包装对象。
例如,通过 GET /v1/orders/{id}
能猜测出返回结果是一个资源对象。
而通过 GET /v1/orders
能猜测出其结果是一个列表。
在某些项目中,复数词汇的 URL 默认返回分页对象,而一些项目会给分页 URL 添加一个 page 后缀,例如 GET /v1/orders/page
。
05 合理实现幂等性
幂等含义:相同输入,应得到相同返回。
GET 天然具有幂等性
PUT DELETE Method 应设计为具有幂等性
POST 根据实际情况进行幂等性设计,例如在消息体中要求调用方传入事务 ID 实现幂等
06 提前设计查询语言或关键字
如果需要实现复杂的查询,可以提前设计一套基于 Query 参数的查询规则,尽可能实现通用的数据库字段查询。
需要考虑:
分页
排序
关键字搜索
与过滤条件
并过滤条件
开闭区间查询
这类实现一般需要结合具体数据库查询框架,例如 JPA、QueryDsl、Mybatis Plus 的 Wrapper 查询能力。
例如下面一个基于 Mybatis Plus 的通用 QueryWrapper:
07 状态码的选用
状态码的选用只用于前端处理一些通用的错误,而具体的业务规则错误统一使用 409(业务规则冲突)来返回,并返回约定的错误码、报错信息。
参考如下:
200 请求成功
201 创建成功
400 数据校验失败
401 用户未认证
403 权限检查失败
404 资源找不到
405 不支持的 Method
409 业务规则冲突
415 不支持的数据请求格式
500 服务器内部错误
503 BFF 转发后端错误
08 批量处理接口
批量处理也是 RESTful 风格 API 不好处理的地方。有一些常见的做法:
由于 URL 使用复数已经代表资源,因此需要增加
/batch
后缀作为 URL 区分使用类似版本号的处理方式在 URL 前增加
/batch
前缀URL 上可以不区分,在传入参数的格式上区分。例如,通过传入一个列表表达批量创建用户。
方案 2 看似更符合 RESTful API 风格,实际上非常容易让人困惑,因为无法通过 URL 语义区分批量接口;方案 2 的问题是,批量接口并不常见,使用前缀会影响很多接口的语义。
在一些文章中,还会区分 bulk 和 batch 的区别,认为 bulk 是对多个资源处理同样的操作,而 batch 是针对多个资源处理不同的操作。
当然在实践中我们不用如此区分,但在微服务环境下一些基础服务往往需要提供批量接口,避免循环调用带来的性能问题。
一些分页接口的例子:
批量创建订单
POST /v1/orders/batch
批量提交订单
POST /v1/orders/batch-submit
09 动词名词化技巧和场景
有些情况下动词 API 在充分建模的情况下也可以名词化。
比如登录的接口,我们在建模充分后识别到登录行为本质上创建了一个会话或者凭证,于是可以将:
POST /v1/users/login
改写为:
POST /v1/authorizes
另外一个例子,在金融领域需要对资产进行估值,看似应该使用:
POST /v1/assets/calculate
其实应该改写为:
POST /v1/capital-rating-transactions
每次评估后都会产生一次事务 ID。
10 区分成功和错误的返回结果
当 API 报错时,有些做法是使用一个容器包装错误信息和成功的消息体。
例如:
这种做法相当于再次封装了 payload 会显得有些冗余,主流的 API 设计一般是: 当返回 HTTP 代码为 2xx 时,返回成功的对象,当使用其他代码时返回错误的对象。
错误的对象一般包含:
业务意义的错误码
错误消息
错误的详细对象,用于某些特殊报错需要返回更多信息
11 创建和更新只返回必要信息
在讨论中,有一个话题是创建和更新操作是否需要返回处理后的对象?
返回的好处是,在编写 API 测试时方便取数,另外也可以复用详情的返回模型。
不返回的好处时,可以简化开发工作,仅仅返回必要的字段和数据,前端需要获取详细数据,可以调用查看相关接口。
同时,不返回全量信息也可以在部分场景提高性能,避免组装、传输过多数据。
12 可以参考的 API 规范和示例
除了前面的建议外,我们也经常参考一些真实的 API 设计规范,下面是一些常见可参考模仿的 API 设计示例:
https://docs.github.com/en/rest?apiVersion=2022-11-28
https://jsonapi.org/
https://wiki.onap.org/display/DW/RESTful+API+Design+Specification
参考资料
https://en.wikipedia.org/wiki/Representational_state_transfer
https://www.codementor.io/blog/batch-endpoints-6olbjay1hd
https://medium.com/paypal-tech/batch-an-api-to-bundle-multiple-paypal-rest-operations-6af6006e002
版权声明: 本文为 InfoQ 作者【少个分号】的原创文章。
原文链接:【http://xie.infoq.cn/article/b7cbe1e1f71fd847c77956506】。文章转载请联系作者。
评论