再谈RESTAPI最佳实践
近一年半,我参与了两到三个项目的工作,这些项目涉及到大量供“外部”使用的REST API,稍后我们会看到为什么要将“外部”这个词放在引号之中。在项目工作期间,我不得不对这些API进行反复地设计,再设计和重构,这篇文章是我对Rest API最佳实践的一些个人看法,希望读者能够从中获益。
更好、更早地设计
对于很多语言来说,实现REST Service是一项极其微不足道的任务。换言之,无论你选择什么底层框架,只要辅以少量配置和代码,你可以在一小时之内就拥有一个REST Service。虽然对于缺乏经验的人来说,这确实很方便,但它也很容易让你迅速写出一个质量低下的API。因此,在你编写代码之前,先留出一分钟的时间思考一下,试着去设计你的API,花足够的时间去理解业务范畴,判断客户端需要从你的系统中获取什么。举个例子,如果你的系统是针对一群硬币收藏家所建立的数据库,此时你需要决定的是:你是否允许客户端添加新的硬币,或者仅仅允许取出原有的硬币;客户需要什么样的查询方式;如果遇上涉及大量数据检索的请求,你如何处理它?尽早地回答这些问题能够帮助你开发出更贴近用户需求的API。
名称与方法
现在已经很有多关于资源(Resource)命名和组织的讨论了,在这里我基于自己的经验再老调重弹一下,以下是三种易于遵循的规范。
1. 只使用名词:举个例子,如果你想提供一项在数据库中搜索硬币的服务,要避免将端点(Endpoint)命名为/searchCoins或/findCoins或/getAllCoins 等等,一个简单的/coins就已经足够了,当客户端发送一个GET请求的时候,可以获得所有有效硬币的集合。类似的,如果你想提供一项在数据库中添加硬币的服务,要避免使用诸如/addCoin或/saveCoin或/insertCointToDatabase这样的名称,你可以使用与上面相同的资源名称,要改变的仅仅是用POST请求代替GET请求。同样地,对于更新硬币,可以使用PUT请求。
2. 如果需要获取单个硬币,又应该怎么做呢?我所建议的最佳方式是在端点中加入一个参数,比如说客户端需要拿到一个ID是20的硬币,那么发送一个请求到/coins/20就足够了。我们再来看一个更复杂的例子,如果要让客户端能够为每个硬币添加一张图片,一个快速而丑陋的方式是/addCoinImage或/addNewImageToCoin等等,一个稍好一点的方式是/coins/addImage,但是正如我之前所说的,不应该有任何动词存在。还记得我们之前提到的获取某种硬币的方法吗?我们可以将其稍微增强一下,发送POST请求给/coins/20/images如何?目前看起来很不错。不过天下没有完美的事物,假设一下,如果我们要让一些超级用户能够从系统中删除硬币,根据我们之前的讨论,一个简单的DELETE请求发送给/coins/{id}就足够了,但是请你想一下,如果{id}仅仅是COINS表中的一个顺序编号,那会产生多大的问题?某人可以轻易地一个接一个的发送DELETE请求,最后系统中所有的数据全没了。我想说的重点是,使用标识符作为请求参数是不错,但是前提是这些标识符必须很难猜测或根本无法猜测。所以,如果你想要用一串序号去确定一个实体,那就忘了这种实现吧。我的建议是,不要使用资源参数,直接发送一个DELETE请求给/coins,结合一个request body(比如json),其中含有足够的参数能够定位所要被删除的实体即可。
3. 尽可能使用特定领域的名称。如果你的业务域中有一群硬币收藏家(Coin Collectors),那么当你设计API的时候,应当使用collectors这个词,而不是users或accounts。要避免使用一些意义过于宽泛的名称,这些名称不能表示什么,到了客户端又容易产生误解。对于请求参数的命名,道理也是一样的。另外,强烈建议给请求参数取一个尽可能短,同时又有意义的名称,举个例子,如果你想要查找在某一指定年份发行的硬币,一个很赞的参数名称是issueYear,比较典型的反例是:year(意义不明确),yearOfFirstIssue(包含无用信息)。
错误处理和响应
对于这个话题,我的经验是让客户端在每次发送请求后,无论结果是成功还是失败,都能获得相同格式的json响应,这将会给客户端处理带来极大的帮助。举个例子,你想要添加一个新的硬币,向/coins发送POST请求,一个成功的响应包含以下json文档:
1
2
3
4
5
6
7
8
|
{ "meta" :{ "code" :200 }, "data" :{ "coinId" : "a7sad-123kk-223" } } |
一个错误的响应可能是这样的:
1
2
3
4
5
6
7
8
9
|
{ "meta" :{ "code" :60001, "error" : "Can not add coin" , "info" : "Missing one ore more required fields" }, "data" :{ } } |
请注意,对所有可能的结果(成功或失败),json响应的文档都具备相同的结构,其中有两种基本元素:meta和data,meta包含结果信息,在出错的情况下,其中还会包含一个特殊的错误码(error code),在错误码之后,”error”表示出错的内容,”info”表示出错的具体描述;data是可选的,包含从服务器返回的所有数据,就拿上面的例子来说,当添加硬币成功后,服务器会返回一个唯一的自动生成的标识符,如果有错误,这项就为空。这种做法的优势是,对于同一个API的各种服务类型和结果,客户端都可以采用相同的方式进行处理。此外,当有意外情况发生时,我们也可以传递一些额外的信息,正如上面例子中所展示的,”error”传达信息,”info”记录日志。我们还有一种选择,可以基于错误码去处理响应,只要明确每个数字的含义即可,请注意这些数字并非http状态码,你依然要为每个请求返回正确的http状态码(如400、401等)。
在我们讨论下一节之前,我想强调另一件值得重视的事,假设我们不允许删除硬币,但是客户端尝试向/coins/{id}发送一个DELETE请求,通常情况下Web容器会返回一个405的状态码,但我发现,如果我们对这些响应进行过滤并返回相同的json文档,会很有帮助。比如我们可以返回:
1
2
3
4
5
6
7
8
9
|
{ "meta" :{ "code" :405, "error" : "Method not allowed for the /coins/{id} resource" , "info" : "Method DELETE is not allowed for that resource. Available methods : GET, POST, OPTIONS" }, "data" :{ } } |
这比原来好多了,不是吗?现在,响应内容不但包含原有的信息(405状态码),还通知客户端该资源可用的方法。
文档
最后但也是最重要的一点,花一点时间,提供一份专业的、对开发人员友好的文档,并保证及时更新,一份过期文档的危害性比没有文档更甚。你可以使用一些开源免费的工具对你的API进行文档化。再好一点的做法是,对每一项资源的使用方式都能提供范例,对成功或错误的响应都能提供预期结果。不要忘了,在最后要记录下每一个错误码并提供完整的信息,这样客户端才能在错误发生时做出反应,有一些客户端不会理会你的响应内容,它们会根据你的错误码自行提供信息。
我还有若干个更为实用的建议待写,特别是关于API的版本控制和安全性方面的建议,但我想它们更适合在另一篇博文中进行探讨。
http://blog.jobbole.com/70511/
再谈RESTAPI最佳实践的更多相关文章
- 再谈 APISIX 高性能实践
2019 年 8 月 31 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·成都站,APISIX 主要作者王院生在活动上做了<APISIX ...
- 浅谈ABP最佳实践
目录 ABP概念简述 ABP在[事务操作]上的简便性 ABP在[关联查询]上的“美”和“坑” ABP的[参数验证]方式 ABP概念简述 ABP是“ASP.NET Boilerplate Project ...
- MaxCompute 构建企业云数据仓库CDW的最佳实践建议
在本文中阿里云资深产品专家云郎分享了基于阿里云 MaxCompute 构建企业云数据仓库CDW的最佳实践建议. 本文内容根据演讲视频以及PPT整理而成. 大家下午好,我是云郎,之前在甲骨文做企业架构师 ...
- [转载]再谈PostgreSQL的膨胀和vacuum机制及最佳实践
本文转载自 www.postgres.cn 下的文章: 再谈PostgreSQL的膨胀和vacuum机制及最佳实践http://www.postgres.cn/news/viewone/1/390 还 ...
- 【转】.NET(C#):浅谈程序集清单资源和RESX资源 关于单元测试的思考--Asp.Net Core单元测试最佳实践 封装自己的dapper lambda扩展-设计篇 编写自己的dapper lambda扩展-使用篇 正确理解CAP定理 Quartz.NET的使用(附源码) 整理自己的.net工具库 GC的前世与今生 Visual Studio Package 插件开发之自动生
[转].NET(C#):浅谈程序集清单资源和RESX资源 目录 程序集清单资源 RESX资源文件 使用ResourceReader和ResourceSet解析二进制资源文件 使用ResourceM ...
- C++ Primer 学习笔记_32_STL实践与分析(6) --再谈string类型(下)
STL实践与分析 --再谈string类型(下) 四.string类型的查找操作 string类型提供了6种查找函数,每种函数以不同形式的find命名.这些操作所有返回string::size_typ ...
- C++ Primer 学习笔记_44_STL实践与分析(18)--再谈迭代器【下】
STL实践与分析 --再谈迭代器[下] 三.反向迭代器[续:习题] //P355 习题11.19 int main() { vector<int> iVec; for (vector< ...
- C++ Primer 学习笔记_43_STL实践与分析(17)--再谈迭代器【中】
STL实践与分析 --再谈迭代器[中] 二.iostream迭代[续] 3.ostream_iterator对象和ostream_iterator对象的使用 能够使用ostream_iterator对 ...
- 《开源安全运维平台OSSIM最佳实践》
<开源安全运维平台OSSIM最佳实践> 经多年潜心研究开源技术,历时三年创作的<开源安全运维平台OSSIM最佳实践>一书即将出版.该书用80多万字记录了,作者10多年的IT行业 ...
随机推荐
- KVM-Introduce
相信非常多的人对虚拟机并不陌生,眼下也有非常多优秀的虚拟机软件,比如:VMware, VirtualBox, Xen, KVM等.而本文的主要内容是介绍KVM. KVM: Kernel Based V ...
- pytest文档2-用例运行规则
用例设计原则 文件名以test_*.py文件和*_test.py 以test_开头的函数 以Test开头的类 以test_开头的方法 所有的包pakege必须要有__init__.py文件 help帮 ...
- 使用BabeLua3.x在cocos2d-x中编辑和调试Lua
BabeLua是一款基于VS2012/2013的Lua集成开发环境,具有Lua语法高亮,语法检查,自动补全,快速搜索,注入宿主程序内对Lua脚本进行调试,设置断点观察变量值,查看堆栈信息等功能. 如何 ...
- (原创)2. WPF中的依赖属性之二
1 依赖属性 1.1 依赖属性最终值的选用 WPF属性系统对依赖属性操作的基本步骤如下: 第一,确定Base Value,对同一个属性的赋值可能发生在很多地方.还用Button的宽度来进行举例,可能在 ...
- Scurm 术语
角色 Product Owner Scrum Master Team 工件(Backlog) Product Backlog Sprint Backlog Burndown Backlog 活动 Sp ...
- CUDA使用Event进行程序计时
GPGPU是众核设备,包含大量的计算单元,实现超高速的并行. 使用CUDA在nvidia显卡上面编程时,可以使用CUDA提供的Event进行程序计时. 当然,每种编程语言基本都提供了获取系统时间的函数 ...
- Oracle数据库导入dmp文件报错处理方法
在向oracle数据库执行导入命令的时候报错,错误如下,大概意思是TNS中找不到服务名 下面说一下解决步骤 1:进入oracle用户,使用cat查看.bash_profile文件,找到ORACLE_H ...
- 【SSH三大框架】Hibernate基础第五篇:利用Hibernate完毕简单的CRUD操作
这里利用Hibernate操作数据库完毕简单的CRUD操作. 首先,我们须要先写一个javabean: package cn.itcast.domain; import java.util.Date; ...
- 裸裸的线段树(hdu 1754)
线段树的第一发. 哪天忘了还能够让自己找找回顾. 线段树操作: build : 建树. update:点改动: query:查询 Input 在每一个測试的第一行,有两个正整数 N 和 M ( 0& ...
- EXPDP/IMPDP与EXP/IMP在不同用户和表空间之间迁移数据的实现方法
1. EXPDP/IMPDP方式 SQL> create user zlm identified by zlm; User created. SQL> grant connect,reso ...