写点什么

系统设计 | RESTful API 使用问题和建议

作者:少个分号
  • 2023-09-03
    上海
  • 本文字数:3273 字

    阅读完需:约 11 分钟

文 | 少个分号 (转载请注明出处)

公众号: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:



// 查询条件的父类public class QueryCondition { private String name; private Integer age;}
// 为通用字段生成查询 Wrapperpublic class MyMapper<T> extends BaseMapper<T> { public List<T> queryByCondition(QueryCondition condition) { QueryWrapper<T> queryWrapper = new QueryWrapper<>(); LambdaQueryWrapper<T> lambdaQueryWrapper = queryWrapper.lambda();
// 根据条件动态构建查询 if (condition.getName() != null) { lambdaQueryWrapper.like(T::getName, condition.getName()); }
if (condition.getAge() != null) { lambdaQueryWrapper.eq(T::getAge, condition.getAge()); }
// 执行查询 return selectList(queryWrapper); }}
复制代码

07 状态码的选用

状态码的选用只用于前端处理一些通用的错误,而具体的业务规则错误统一使用 409(业务规则冲突)来返回,并返回约定的错误码、报错信息。


参考如下:


  • 200 请求成功

  • 201 创建成功

  • 400 数据校验失败

  • 401 用户未认证

  • 403 权限检查失败

  • 404 资源找不到

  • 405 不支持的 Method

  • 409 业务规则冲突

  • 415 不支持的数据请求格式

  • 500 服务器内部错误

  • 503 BFF 转发后端错误

08 批量处理接口

批量处理也是 RESTful 风格 API 不好处理的地方。有一些常见的做法:


  1. 由于 URL 使用复数已经代表资源,因此需要增加 /batch 后缀作为 URL 区分

  2. 使用类似版本号的处理方式在 URL 前增加 /batch 前缀

  3. 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 报错时,有些做法是使用一个容器包装错误信息和成功的消息体。


例如:


{    "code": "xxx"    "data": {}    "error": {}}
复制代码


这种做法相当于再次封装了 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

发布于: 刚刚阅读数: 6
用户头像

少个分号

关注

还未添加个人签名 2019-09-26 加入

还未添加个人简介

评论

发布
暂无评论
系统设计 | RESTful API 使用问题和建议_系统设计_少个分号_InfoQ写作社区