设计 REST API 的13个最佳实践
写在前面
之所以翻译这篇文章,是因为自从成为一名前端码农之后,调接口这件事情就成为了家常便饭,并且,还伴随着无数的争论与无奈。编写友好的 restful api
不论对于你的同事,还是将来作为第三方服务调用接口的用户来说,都显得至关重要。关于 restful api
本身以及设计原则,我陆陆续续也看过很多的文章和书籍,在读过原文后,感觉文中指出的 13 点最佳实践还是比较全面的且具有参考意义的,因此翻译出来分享给大家。如有错误,还望指正。
由于我一般倾向于意译,关于原文中的开头语或者一些与之无关的内容,我就省略掉了,毕竟时间是金钱,英语好并且能科学上网的朋友我建议还是看原文,以免造成理解上的误差。
1. 了解应用于 REST 之上的 HTTP 知识
如果你想要构建设计优良的 REST API,了解一些关于 HTTP 协议的基础知识是很有帮助的,毕竟磨刀不误砍材工。
在 MDN 上有很多质量不错的文档介绍 HTTP。但是,就 REST API 设计本身而言,所涉及到的 HTTP 知识要点大概包含以下几条:
- HTTP 中包含动词(或方法):
GET
、POST
、PUT
、PATCH
还有DELETE
是最常用的。 - REST 是面向资源的,一个资源被一个 URI 所标识,比如
/articles/
。 - 端点(endpoint),一般指动词与 URI 的组合,比如
GET: /articles/
。 - 一个端点可以被解释为对某种资源进行的某个动作。比如,
POST: /articles
可能代表“创建一个新的 article”。 - 在业务领域,我们常常可以将动词和 CRUD(增删查改)关联起来:
GET
代表查,POST
代表增,PUT
和PATCH
代表改(注: PUT 通常代表整体更新,而 PATCH 代表局部更新),而DELETE
代表删。
当然了,你可以将 HTTP 协议中所提供的任何东西应用于 REST API 的设计之中,但以上这些是比较基础的,因此时刻将它们记在脑海中是很有必要的。
2. 不要返回纯文本
虽然返回 JSON 数据格式的数据不是 REST 架构规范强制限定的,但大多 REST API 都遵循这条准则。
但是,仅仅返回 JSON 数据格式的数据还是不够的,你还需要指定返回 body 的头部,比如 Content-Type
,它的值必须指定为 application/json
。这一点对于程序化客户端尤为重要(比如通过 python 的 requests
模块来与 api 进行交互)—— 这些程序是否对返回数据进行正确解码取决于这个头部。
注:通常而言,对于浏览器来说,这似乎不是问题,因为浏览器一般都自带内容嗅探机制,但为了保持一致性,还是在响应中设置这个头部比较妥当。
3. 避免在 URI 中使用动词
如果你理解了第 1 条最佳实践所传达的意思,那么你现在就会明白不要将动词放入 REST API 的 URI 中。这是因为 HTTP 的动词已经足以描述执行于资源的业务逻辑操作了。
举个例子,当你想要提供一个针对某个 article 提供 banner 图片并返回的接口时,可能会实现如下格式的接口:
GET: /articles/:slug/generateBanner/
这里 GET 已经说明了这个接口是在做读的操作,因此,可以简化为:
GET: /articles/:slug/banner/
类似的,如果这个端口是要创建一个 article:
// 不要这么做
POST: /articles/createNewArticle/
// 这才是最佳实践
POST: /articles/
尝试用 HTTP 的动词来描述所涉及的业务逻辑操作。
4. 使用复数的名词来描述资源
一些时候,使用资源的复数形式还是单数形式确实存在一定的困扰,比如使用 /article/:id/
更好还是使用 /articles/:id/
更好呢?
这里我推荐使用后者。为什么呢?因为复数形式可以满足所有类型端点的需求。
单数形式的 GET /article/2/
看起来还是不错的,但是如果是 GET /article/
呢?你能够仅通过字面信息来区分这个接口是返回某个 article 还是多个呢?
因此,为了避免有单数命名造成的歧义性,并尽可能的保持一致性,使用复数形式,比如:
GET: /articles/2/
POST: /articles/
...
5. 在响应中返回错误详情
当 API 服务器处理错误时,如果能够在返回的 JSON body 中包含错误信息,对于接口调用者来说,会一定程度上帮助他们完成调试。比如对于常见的提交表单,当遇到如下错误信息时:
{
"error": "Invalid payoad.",
"detail": {
"surname": "This field is required."
}
}
接口调用者很快就是明白发生错误的原因。
6. 小心 status code
这一点可能是最重要、最重要、最重要的一点,可能也是这篇文章中,唯一你需要记住的那一点。
你可能知道,HTTP 中你可以返回带有 200 状态码的错误响应,但这是十分糟糕的。不要这么做,你应当返回与返回错误类型相一致的具有一定含义的状态码。
聪明的读者可能会说,我按照第 5 点最佳实践来提供足够详细的信息,难道不行吗?当然可以,不过让我讲一个故事:
我曾经使用过一个 API,对于它返回的所有响应的状态码均是 200 OK
,同时通过响应数据中的 status
字段来表示当前的请求是否成功,比如:
{
"status": "success",
"data": {}
}
所以,虽然状态码是 200 OK
,但我却不能绝对确定请求是否成功,事实上,当错误发生时,这个 API 会按如下代码片段返回响应:
HTTP/1.1 200 OK
Content-Type: text/html
{
"status": "failure",
"data": {
"error": "Expected at least two items in list."
}
}
头部还是 text/html
,因为它同时返回了一些 HTML 片段。
正因为这样,我不得不在检查响应状态码正确的同时,还需校验这个具有特殊含义的 status
字段的值,才可以放心的处理响应返回的 data
。
这种设计的一个真正坏处在于,它打破了接口与调用者之间的“信任”,因为你可能会担心这个接口对你撒谎(注:言外之意就是,由于特设的字段可能会改变,因此增加了不可靠性)。
所以,使用正确的状态码,同时仅在响应的 body 中返回错误信息,并设置正确的头部,比如:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "Expected at least two items in list."
}
7. 保持 status code 的一致性
当你掌握了正确使用状态码之后,就应该努力使它们具有一致性。
比如,如果一个 POST 类型的端点返回 201 Created
,那么所有的 POST 端点都应返回同样的状态码。这样做的好处在于,调用者无需在意端点返回的状态码取决于某种特殊条件,也就形成了一致性。如果有特殊情况,请在文档中显著地说明它们。
下面是我推荐的与动词相对应的状态码:
GET: 200 OK
POST: 201 Created
PUT: 200 OK
PATCH: 200 OK
DELETE: 204 No Content
blog.florimondmanca.com/restful-api…
8. 不要嵌套资源
使用 REST API 获取资源数据,通常情况下会直接获取多个或者单个,但当我们需要获取相关联的资源时,该怎么做呢?
比如说,我们期望获取作者为某个 author 的 article 列表 —— 假设 authro 的 id=12
。这里提供两种方案:
第一种方案通过在 URI 中,将嵌套的资源放在所关联的资源后边来进行描述,比如:
GET: /authors/12/articles/
一些人推荐这种方案的理由是,这种形式的 URI 一定程度上描述了 author 与 article 之间的一对多关系。但与此同时,结合第 4 点最佳实践,我们就不太能够分清当前端点返回的数据到底是 author 类型还是 article 类型。
这里有一篇文章,详细阐述了扁平化形式优于嵌套形式,因此一定有更好的方法,这就是下面的第二种方案:
GET: /articles/?author_id=12
直接将筛选 article 的逻辑抽离为 querystring 即可,这样的 URI 相比之前,更加清晰地描述了“获取所有 author(id=12) 的 article”的意思。
9. 优雅地处理尾部斜杠
一个好的 URI 中是否应当包含尾部斜杠,并不具有探讨价值,选择一种更倾向的风格并保持一致性即可,同时当客户端误用尾部斜杠时,提供重定向响应。
我再来讲我自己的一个故事。某天,我在将某个 API 端点集成到项目中,但是我总是收到 500 Internal Error
的错误,我调用的端点差不多看起来这样:
POST: /entities
调试一段时间之后,我几乎崩溃了,因为我根本不知道我哪里做错了,直到我发现服务器之所以报 500 的错误,是因为我粗心丢掉了尾部斜杠(注:这种经历人人都会遇到,我在 SF 上遇过无数次类似的问题),当我把 URI 改成:
POST: /entities/
之后,一切正常运转。
当然,大多数的 web 框架都针对 URL 是否包含尾部斜杠,进行了优雅地处理并提供定制选项,如果可以的话,找到它并开启这项功能。
10. 使用 querystring 来完成筛选和分页功能
大部分情况下,一个简单的端点没有办法满足负责业务场景。
你的用户可能想要获取满足一定条件下的某些数据集合 ,同时为了保证性能,仅仅获取这个集合下的一个子集。换言之,这通常叫作筛选功能和分页功能:
- 筛选:用户可以提供额外的属性来控制返回的数据集合
- 分页:获取数据集合的子集,最简单的分页是基于分页个数的分页,它由
page
和page_size
来决定
那么问题来了,我们如何将这两项功能与 RESTful API 结合在一起呢?
答案当然是通过 querystring。对于分页,很显然使用这种方式再合适不过了,比如:
GET: /articles/?page=1&page_size=10
但对于筛选,你可能会犯第 8 点最佳实践中所指出的问题,比如获取处于 published 状态的 article 列表:
GET: /articles/published/
除了之前提出的问题外,这里还涉及一个设计上的问题,就是 published 本身不是资源,它仅仅是资源的特征,类似这种特征字段,应该将它们放到 querystring 中:
GET: /articles/?published=true&page=2&page_size=20
更加优雅、清晰,不是吗?
11. 分清 401 和 403
当我们遇到 API 中关于安全的错误提示时,很容易混淆这两个不同类型的错误,认证和授权(比如权限相关)—— 老实讲,我自己也经常搞混。
这里是我自己总结的备忘录,它阐述了我如何在实际情况下,区分它们:
- 用户是否未提供身份验证凭据?认证是否还有效?这种类型的错误一般是未认证(
401 Unauthorized
)。 - 用户经过了正常的身份验证,但没有访问资源所需的权限?这种一般是未授权(
403 Forbidden
)
12. 巧用 202 Accepted
我发现 202 Accepted
在某些场合是 201 Created
的一个非常便捷的替代方案,这个状态码的含义是:
服务器已经接受了你的请求,但是到目前为止还未创建新的资源,但一切仍处于正常状态。
我分享两种特别适合使用 202 Accepted
状态码的业务场景:
- 如果资源是经过位于将来一系列处理流程之后才创建的,比如当某项作业完成时
- 如果资源已经存在,但这是理想状态,因此不应该被识别为一个错误时
13. 采用 REST API 定制化的框架
作为最后一个最佳实践,让我们来探讨这样一个问题:你如何在 API 的实施中,实践最佳实践呢?
通常的情况是这样的,你想要快速创建一个 API 以便一些服务可以互相访问彼此。Python 开发者可能马上掏出了 Flask,而 JS 开发者也不甘示弱,祭出了 Express,他们会使用实现一些简单的 routes 来处理 HTTP 请求。
但这样做的问题是,通常,web 框架并不是针对构建 REST API 服务而专门存在的,换言之,Flask 和 Express 是两个十分通用的框架,但它们并非特别适合用于构建 REST API 服务。因此,你必须采取额外的步骤来实施 API 中的最佳实践,但大多数情况下,由于懒惰或者时间紧张等因素,意味着你不会投入过多精力在这些方面 —— 然后给你的用户提供了一个古怪的 API 端点。
解决方案十分简单:工欲善其事,必先利其器,掌握并使用正确的工作才是最好的方案。在各种语言中,许多专门用于构建 REST API 服务的新框架已经出现了,它们可以帮助你在不牺牲生产力的情况下,轻松地完成工作,同时遵循最佳实践。在 Python 中,我发现的最好的 API 框架之一是 Falcon。它与 Flask 一样简单,非常高效,十分适合构建 REST API 服务。如果你更喜欢 Django 的话,使用 Django REST Framework就足够了,虽然框架不是那么直观(注:按我的理解应该是说不太容易上手,但是我不这么认为),但功能非常强大。在 NodeJS 中,Restify 似乎也是一个不错的选择,尽管我还没有尝试过。我强烈建议你给这些框架一个机会!它们将帮助你构建规范,优雅且设计良好的 REST API 服务。
总结
我们都应致力于让调用 API 这件事成为一种乐趣。希望本文能使你了解到在构建更好的 REST API 服务的过程中,涉及到的一些建议和技巧。对我而言,应该把这些最佳实践归结为三点,分别是良好的语义,简洁和合理性。
PS:分析一个好消息
同学,你造吗?阿里云和腾讯云已白菜价,最新活动,云服务器低至不到300元/年。这里有一份云计算优惠活动列表,来不及解释了,赶紧上车!
作者:HaoliangWu
链接:https://juejin.im/post/5c1ba471f265da61257812bf
设计 REST API 的13个最佳实践的更多相关文章
- restful api的10个最佳实践
Web API在过去的几年里非常盛行,因为它有着语法简单.规范化和轻量级的优点,因为得到广泛的推崇,很多过往的技术手段都慢慢转换为使用Web API来开发.而Web API通常使用的设计方式是REST ...
- Atitit.异常的设计原理与 策略处理 java 最佳实践 p93
Atitit.异常的设计原理与 策略处理 java 最佳实践 p93 1 异常方面的使用准则,答案是:: 2 1.1 普通项目优先使用异常取代返回值,如果开发类库方面的项目,最好异常机制与返回值都提供 ...
- 基于ABP落地领域驱动设计-05.实体创建和更新最佳实践
目录 系列文章 数据传输对象 输入DTO最佳实践 不要在输入DTO中定义不使用的属性 不要重用输入DTO 输入DTO中验证逻辑 输出DTO最佳实践 对象映射 学习帮助 系列文章 基于ABP落地领域驱动 ...
- .NET API 接口数据传输加密最佳实践
.NET API 接口数据传输加密最佳实践 我们在做 Api 接口时,相信一定会有接触到要给传输的请求 body 的内容进行加密传输.其目的就是为了防止一些敏感的内容直接被 UI 层查看或篡改. 其实 ...
- 保护REST API/Web服务的最佳实践
在设计REST API或服务时,是否存在处理安全性(身份验证,授权,身份管理)的最佳实践? 在构建SOAP API时,您可以使用WS-Security作为指导,有关该主题的文献很多.我发现了有关保护R ...
- 编写 Node.js Rest API 的 10 个最佳实践
Node.js 除了用来编写 WEB 应用之外,还可以用来编写 API 服务,我们在本文中会介绍编写 Node.js Rest API 的最佳实践,包括如何命名路由.进行认证和测试等话题,内容摘要如下 ...
- 13. ZooKeeper最佳实践
以下列举了运行和管理ZooKeeper ensemble的一些最佳实践: ZooKeeper数据目录包含快照和事务日志文件.如果autopurge选项未启用,定期清理目录是一个好习惯.另外,管理员可能 ...
- Java代码通过API操作HBase的最佳实践
HBase提供了丰富的API.这使得用Java连接HBase非常方便. 有时候大家会使用HTable table=new HTable(config,tablename);的方式来实例化一个HTabl ...
- SolrJ API 官方文档最佳实践
以下内容译自Solr Wiki官方文档,版权没有,随意转载. Solrj 是一个访问solr的Java客户端.它提供了一个java接口用于添加更新和查询solr索引.本页面介绍SolrJ最新版本1.4 ...
随机推荐
- android adb源码分析(5)【转】
本文转载自:http://blog.csdn.net/xgbing/article/details/52096880 本篇以“adb devices"命令为例,跟踪代码的执行流程. (1) ...
- LoadRunner使用动态链接库技术
什么是动态库? 动态库一般又叫动态链接库英文为DLL,是Dynamic Link Library 的缩写形式,DLL是一个包含可由多个程序同时使用的代码和数据的库,DLL不是可执行文件.动态链接提供了 ...
- html5--6-14 CSS3中的颜色表示方式
html5--6-14 CSS3中的颜色表示方式 实例 每个参数 (red.green 以及 blue) 定义颜色的强度,可以是介于 0 与 255 之间的整数,或者是百分比值(从 0% 到 100% ...
- I.MX6 按键开关机 PMIC 检测
/************************************************************************* * I.MX6 按键开关机 PMIC 检测 * 说 ...
- 「LuoguP1799」 数列_NOI导刊2010提高(06)
题目描述 虽然msh长大了,但她还是很喜欢找点游戏自娱自乐.有一天,她在纸上写了一串数字:1,1,2,5,4.接着她擦掉了一个l,结果发现剩下1,2,4都在自己所在的位置上,即1在第1位,2在第2位, ...
- Ruby nokogiri 解析xml的简单实例
require 'nokogiri'XML_FILE = "C:\\Users\\chenpassion\\Desktop\\20130806.xml"xml = Nokogiri ...
- LINUX-进程的概念
计算机中,CPU是最宝贵的资源,为了提高CPU的利用率,引入了多道程序设计的概念.当内存中多个程序存在时,如果不对人们熟悉的“程序”的概念加以扩充,就无法刻画多个程序共同运行时系统呈现出的特征. 一. ...
- Java中的Cloneable接口与深拷贝、浅拷贝
Cloneable接口是一个标记接口,也就是没有任何内容,定义如下: 这里分析一下这个接口的用法,clone方法是在Object种定义的,而且是protected型的,只有实现了这个接口,才可以在该类 ...
- ORA-01152: 文件 1 没有从过旧的备份中还原
转自:http://blog.itpub.net/8520577/viewspace-1255794/ 做了一个全备 RMAN> show all; 使用目标数据库控制文件替代恢复目录db_un ...
- perceptron and ANN
%% Perceptron Regression close all clear %%load data x = load('ex4x.dat'); y = load('ex4y.dat'); x=o ...