RESTful 是目前最流行的 API 设计规范,用于 Web 数据接口的设计。
它的大原则容易把握,但是细节不容易做对。本文总结 RESTful 的设计细节,介绍如何设计出易于理解和使用的 API。
RESTful 的核心思想就是,客户端发出的数据操作指令都是"动词 + 宾语"的结构。比如,GET /articles
这个命令,GET
是动词,/articles
是宾语。
动词通常就是五种 HTTP 方法,对应 CRUD 操作。
- GET:读取(Read)
- POST:新建(Create)
- PUT:更新(Update)
- PATCH:更新(Update),通常是部分更新
- DELETE:删除(Delete)
根据 HTTP 规范,动词一律大写。
有些客户端只能使用GET
和POST
这两种方法。服务器必须接受POST
模拟其他三个方法(PUT
、PATCH
、DELETE
)。
这时,客户端发出的 HTTP 请求,要加上X-HTTP-Method-Override
属性,告诉服务器应该使用哪一个动词,覆盖POST
方法。
POST /api/Person/4 HTTP/1.1 X-HTTP-Method-Override: PUT
上面代码中,X-HTTP-Method-Override
指定本次请求的方法是PUT
,而不是POST
。
宾语就是 API 的 URL,是 HTTP 动词作用的对象。它应该是名词,不能是动词。比如,/articles
这个 URL 就是正确的,而下面的 URL 不是名词,所以都是错误的。
- /getAllCars
- /createNewCar
- /deleteAllRedCars
既然 URL 是名词,那么应该使用复数,还是单数?
这没有统一的规定,但是常见的操作是读取一个集合,比如GET /articles
(读取所有文章),这里明显应该是复数。
为了统一起见,建议都使用复数 URL,比如GET /articles/2
要好于GET /article/2
。
常见的情况是,资源需要多级分类,因此很容易写出多级的 URL,比如获取某个作者的某一类文章。
GET /authors/12/categories/2
这种 URL 不利于扩展,语义也不明确,往往要想一会,才能明白含义。
更好的做法是,除了第一级,其他级别都用查询字符串表达。
GET /authors/12?categories=2
下面是另一个例子,查询已发布的文章。你可能会设计成下面的 URL。
GET /articles/published
查询字符串的写法明显更好。
GET /articles?published=true
客户端的每一次请求,服务器都必须给出回应。回应包括 HTTP 状态码和数据两部分。
HTTP 状态码就是一个三位数,分成五个类别。
1xx
:相关信息2xx
:操作成功3xx
:重定向4xx
:客户端错误5xx
:服务器错误
这五大类总共包含100多种状态码,覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。
API 不需要1xx
状态码,下面介绍其他四类状态码的精确含义。
200
状态码表示操作成功,但是不同的方法可以返回更精确的状态码。
- GET: 200 OK
- POST: 201 Created
- PUT: 200 OK
- PATCH: 200 OK
- DELETE: 204 No Content
上面代码中,POST
返回201
状态码,表示生成了新的资源;DELETE
返回204
状态码,表示资源已经不存在。
此外,202 Accepted
状态码表示服务器已经收到请求,但还未进行处理,会在未来再处理,通常用于异步操作。下面是一个例子。
HTTP/1.1 202 Accepted { "task": { "href": "/api/company/job-management/jobs/2130040", "id": "2130040" } }
API 用不到301
状态码(永久重定向)和302
状态码(暂时重定向,307
也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API 级别可以不考虑这两种情况。
API 用到的3xx
状态码,主要是303 See Other
,表示参考另一个 URL。它与302
和307
的含义一样,也是"暂时重定向",区别在于302
和307
用于GET
请求,而303
用于POST
、PUT
和DELETE
请求。收到303
以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。下面是一个例子。
HTTP/1.1 303 See Other Location: /api/orders/12345
4xx
状态码表示客户端错误,主要有下面几种。
400 Bad Request
:服务器不理解客户端的请求,未做任何处理。
401 Unauthorized
:用户未提供身份验证凭据,或者没有通过身份验证。
403 Forbidden
:用户通过了身份验证,但是不具有访问资源所需的权限。
404 Not Found
:所请求的资源不存在,或不可用。
405 Method Not Allowed
:用户已经通过身份验证,但是所用的 HTTP 方法不在他的权限之内。
410 Gone
:所请求的资源已从这个地址转移,不再可用。
415 Unsupported Media Type
:客户端要求的返回格式不支持。比如,API 只能返回 JSON 格式,但是客户端要求返回 XML 格式。
422 Unprocessable Entity
:客户端上传的附件无法处理,导致请求失败。
429 Too Many Requests
:客户端的请求次数超过限额。
5xx
状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。
500 Internal Server Error
:客户端请求有效,服务器处理时发生了意外。
503 Service Unavailable
:服务器无法处理请求,一般用于网站维护状态。
API 返回的数据格式,不应该是纯文本,而应该是一个 JSON 对象,因为这样才能返回标准的结构化数据。所以,服务器回应的 HTTP 头的Content-Type
属性要设为application/json
。
客户端请求时,也要明确告诉服务器,可以接受 JSON 格式,即请求的 HTTP 头的ACCEPT
属性也要设成application/json
。下面是一个例子。
GET /orders/2 HTTP/1.1 Accept: application/json
有一种不恰当的做法是,即使发生错误,也返回200
状态码,把错误信息放在数据体里面,就像下面这样。
HTTP/1.1 200 OK Content-Type: application/json { "status": "failure", "data": { "error": "Expected at least two items in list." } }
上面代码中,解析数据体以后,才能得知操作失败。
这张做法实际上取消了状态码,这是完全不可取的。正确的做法是,状态码反映发生的错误,具体的错误信息放在数据体里面返回。下面是一个例子。
HTTP/1.1 400 Bad Request Content-Type: application/json { "error": "Invalid payoad.", "detail": { "surname": "This field is required." } }
API 的使用者未必知道,URL 是怎么设计的。一个解决方法就是,在回应中,给出相关链接,便于下一步操作。这样的话,用户只要记住一个 URL,就可以发现其他的 URL。这种方法叫做 HATEOAS。
举例来说,GitHub 的 API 都在 api.github.com 这个域名。访问它,就可以得到其他 URL。
{ ... "feeds_url": "https://api.github.com/feeds", "followers_url": "https://api.github.com/user/followers", "following_url": "https://api.github.com/user/following{/target}", "gists_url": "https://api.github.com/gists{/gist_id}", "hub_url": "https://api.github.com/hub", ... }
上面的回应中,挑一个 URL 访问,又可以得到别的 URL。对于用户来说,不需要记住 URL 设计,只要从 api.github.com 一步步查找就可以了。
HATEOAS 的格式没有统一规定,上面例子中,GitHub 将它们与其他属性放在一起。更好的做法应该是,将相关链接与其他属性分开。
HTTP/1.1 200 OK Content-Type: application/json { "status": "In progress", "links": {[ { "rel":"cancel", "method": "delete", "href":"/api/status/12345" } , { "rel":"edit", "method": "put", "href":"/api/status/12345" } ]} }
(完)
维基百科有一个姐妹项目,叫做"维基数据"(Wikidata)。你可以从维基百科左侧边栏点进去。
FFmpeg 是视频处理最常用的开源软件。
有时,Bash 脚本需要创建临时文件或临时目录。
Git 版本管理时,往往需要撤销某些操作。
半卷书 说:
请问 RESTful API 对SEO友好吗?由其是像 GET /authors/12?categories=2这种的url
2018年10月 3日 20:24 | # | 引用
felbry 说:
发现阮老师博客head也新加上了下border。我之前自己加过一段时间,后来觉得还是太丑了,哈哈
2018年10月 3日 21:02 | # | 引用
Alexu 说:
github的api似乎也是倾向于使用多级而不是查询字符串,这么说也不符合最佳实践吗?
2018年10月 3日 21:28 | # | 引用
Jamie 说:
上一篇REST还有印象hhh
2018年10月 4日 01:26 | # | 引用
Godruoyi 说:
大佬来写果然深度不一样,也欢迎大家去看看我总结的 restful api 规范
https://godruoyi.com/posts/resetful-api-design-specifications
2018年10月 4日 13:41 | # | 引用
t 说:
请教一下大家,如果遇到动词不在常见的几种之中,甚至是需要自定义的动词,怎么做比较合理?
2018年10月 4日 15:46 | # | 引用
明达 说:
422说的有点含糊,换个意思说,其实最常用的场景是服务器端表单验证失败
2018年10月 4日 16:44 | # | 引用
明达 说:
引用t的发言:请教一下大家,如果遇到动词不在常见的几种之中,甚至是需要自定义的动词,怎么做比较合理?
这个动词是HTTP固定的吧,其实更多的动词场景,我理解都可以区分成几种,只要是获取信息,都可以用GET,如果是在基本信息表增加记录,就是POST,其他只要是修改,或者是修改关系表这种情况,应该都是UPDATE,update和put其实是有差别的。如果要update的行为很多,我会在后面增加?type= 这类参数,如果要是特别直接的动作,比如upload这种,直接放在最高的级别也ok啊。 abc.com/upload
2018年10月 4日 16:51 | # | 引用
WangNianyi2001 说:
3.1 不要返回纯本文
标题打错啦
2018年10月 5日 10:07 | # | 引用
etworker 说:
请问对于登录操作,可以用restful api的格式吗?如果可以,对应的资源是什么呢?
2018年10月 6日 01:45 | # | 引用
code 说:
引用etworker的发言:请问对于登录操作,可以用restful api的格式吗?如果可以,对应的资源是什么呢?
POST /session
2018年10月 8日 11:26 | # | 引用
fengchang 说:
204 No Content 应该是指没有需要返回给客户端的内容,而不是服务端的内容已经不存在
https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204
2018年10月 8日 17:56 | # | 引用
萌一秒 说:
前几天看完您的js全栈,正好在查REST,最近您就出了,真的厉害!
2018年10月 8日 21:56 | # | 引用
tanglei 说:
我们的实践: 400 用于表示客户端传参错误(或者不完全), 200 有可能也是不正常的响应(当然不能算是错误), 比如用户名或者密码不匹配.
2018年10月 9日 09:55 | # | 引用
xiaohuangmao 说:
通俗易懂 深受启发
2018年10月 9日 17:52 | # | 引用
robinson 说:
关于restful风格和rpc风格的api设计和公司同事有过争论,感觉是主义之争,不会有什么结果。不过关于rest风格,在实际应用中,也遇到过难处理的问题,比如,client验证用户名或者电话是否存在,就不知如何设计怎么好,最后“强行”设计成:GET /users/checking(validating)?username=xx,反倒是,rpc风格,GET /users.check?username=xx是否表达力更强一些?再如,某个操作导致状态更新,总结下,就是对于有很强的“动作”在内的api,应该如何用rest风格设计?这个问题困扰我很久了,望阮老师解惑,先在这里谢过了。
2018年10月13日 10:48 | # | 引用
Shuo Wang 说:
多个资源的关联关系的变更,URL 如何设计比较好?
比如将某个用户加入到某个 Team 中
PUT /users/${user_id}/teams/${team_id}
PUT /users/${user_id}?team_id=${team_id}
如果是第二种,是不是不太好区分 users 和 teams 是两种资源?如果是第一种,就会比较明确一些。
又或者将某个 Team 中的某个用户设置为非激活状态
PUT /users/${user_id}/teams/${team_id}?status=inactive
PUT /users/${user_id}?team_id=${team_id}?status=inactive
2018年10月15日 16:07 | # | 引用
dada0z 说:
公司进行nessus扫描时,报告web server只允许使用GET和POST,不允许使用其他方法。方法覆写也是禁止的。请问,这种冲突,应该如何解决?
2018年10月16日 13:05 | # | 引用
Joshua 说:
@robinson:
可以看阮老师的这篇文章中7、误区,里面有讲述服务的设计。
http://www.ruanyifeng.com/blog/2011/09/restful.html
2018年10月23日 10:16 | # | 引用
李 说:
老师,请问,如果跨域前端能取得到http错误码吗,我们公司前端说跨域的时候只能取到200其他的取不到,所有如果真的取不到,那请问是不是比如:404的时候也要返回200,然后把错误信息和404错误码放在数据体里面。是吗
2018年10月23日 11:56 | # | 引用
Bob 说:
关于1.5节,仅仅举了GET命令的例子,但是对二级资源做POST/PUT/DELETE的时候,是否还可以使用查询字符串表达?
2018年10月24日 17:39 | # | 引用
Rui 说:
对于查询字符串,我们在应用的范式是当定位某种资源时,用多级地址,但当定义response如何返回时,用查询字符串,比如返回是否是paginate的,最大返回多少
2018年10月26日 06:10 | # | 引用
betty 说:
引用半卷书的发言:请问 RESTful API 对SEO友好吗?由其是像 GET /authors/12?categories=2这种的url
这个没关系吧,看你的页面是服务端渲染还是前端渲染吧
2018年10月26日 16:17 | # | 引用
robinson 说:
@Joshua
那篇文章,我也拜读过,但还是有疑惑的,我们是可以向都是名词化靠拢,但这个世界难道都可以“资源化”吗?比如我遇到的问题,检查用户是否存在,难道一定要按用户名查询用户?如果返回了用户,那就是存在?同样的情况还有:验证验证码是否正确。还有订单的情景,我下单后订单状态成为“待发货”,但如果按照“资源化”的思路,应该如何设计呢?“PUT /orders/{id}?action=下单”?还是PUT /orders/{id}?status=代发货?或者/orders/{id}/status/待发货?我感觉后面的这种情况更严重,这样封装性很差,把逻辑交给了下游,有为了rest而rest之嫌,如果是指定action的情况,那么也比较糟糕,难道我们对订单的接口只有四个?其余的都只能通过参数表达?后端实现也会成为一锅粥。还望各位大牛解惑
2018年10月29日 10:47 | # | 引用
陈生 说:
感觉没看懂呀。。。
2018年11月 3日 20:42 | # | 引用
枪骑兵叔叔 说:
勘误下:
3.2里 “Invalid payoad.”
是payload吧,单词拼写错误
2018年11月 8日 15:18 | # | 引用
Lightc 说:
引用robinson的发言:关于restful风格和rpc风格的api设计和公司同事有过争论,感觉是主义之争,不会有什么结果。不过关于rest风格,在实际应用中,也遇到过难处理的问题,比如,client验证用户名或者电话是否存在,就不知如何设计怎么好,最后“强行”设计成:GET /users/checking(validating)?username=xx,反倒是,rpc风格,GET /users.check?username=xx是否表达力更强一些?再如,某个操作导致状态更新,总结下,就是对于有很强的“动作”在内的api,应该如何用rest风格设计?这个问题困扰我很久了,望阮老师解惑,先在这里谢过了。
我觉得这样的设计成这样比较 GET /users/{userName}?c=check ,c代表command的意思,对userName进行check操作
2018年11月29日 16:46 | # | 引用
not3 说:
PUT /user/${user_id}.join-to/team/${team_id}
是否可以
2018年12月19日 23:01 | # | 引用
英武 说:
怎么看都觉得少了点什么,也许功能测试都没有什么问题,各种cornner都要测到,但是性能测试可否详细谈一下?locust?
2018年12月20日 11:46 | # | 引用
not3 说:
比如获取某个作者的某一类文章。
这个例子写的示例语义上不太好,返回的资源其实是文章,那么应该表述为
GET /articles?authorId=12&categoryId=2
本来就没有层级关系
另外,某类的所有文章,某作者的所有文章
GET /category/2/articles
GET /author/12/articles
2018年12月20日 14:44 | # | 引用
binger 说:
有个疑问,发生错误了状态码不能为200,应该给出具体状态码,错误放在返回值中,反正都是要解析返回值的,状态码200不是少判断一步状态码么。。。
2019年1月31日 15:49 | # | 引用
郑诚 说:
为什么没有502
2019年3月12日 08:30 | # | 引用
Xanthuim 说:
引用李的发言:老师,请问,如果跨域前端能取得到http错误码吗,我们公司前端说跨域的时候只能取到200其他的取不到,所有如果真的取不到,那请问是不是比如:404的时候也要返回200,然后把错误信息和404错误码放在数据体里面。是吗
怎么可能取不到,只要是基于http协议的都可以。只是他们没有这么做,要么是前端技术low,对于这种你就把这篇文章丢给他即可,其他什么都不要说。
2019年3月20日 14:38 | # | 引用
Xanthuim 说:
引用郑诚 的发言:为什么没有502
文章都说的很清楚,对于服务端异常,一般不会透露过多的信息:
5xx状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。
当然你也要把更多的异常信息往外抛,看你了,只是不建议。
2019年3月20日 14:40 | # | 引用
Xanthuim 说:
引用binger的发言:有个疑问,发生错误了状态码不能为200,应该给出具体状态码,错误放在返回值中,反正都是要解析返回值的,状态码200不是少判断一步状态码么。。。
你可以返回实际的状态码,比如你现在要返回的HTTP状态码是404,那么返回的JSON中状态码也可以用404,其他也是类似的。
2019年3月20日 14:42 | # | 引用
Xanthuim 说:
@Shuo Wang:
你这种就不应该放在一起,分开写
2019年3月20日 14:44 | # | 引用
Xanthuim 说:
引用fengchang的发言:204 No Content 应该是指没有需要返回给客户端的内容,而不是服务端的内容已经不存在
https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204
删除了是没有啊,表示这个资源已经不存在,用204没毛病。其实没必要太钻牛角尖,能基本表示清楚就可以了。
2019年3月20日 14:49 | # | 引用
有一点梦想的咸鱼 说:
api呈现给用户可以继承swgger。哈哈哈哈......手写API文档的岁月一去不复返
2019年3月26日 00:26 | # | 引用
pto2 说:
@robinson:
你把“验证存在性”理解为“尝试获取”就好办了,直接GET /users/你想获取的用户名 ,不存在就直接返回不存在就是了。
2019年4月 4日 15:11 | # | 引用
血火 说:
老阮的你可真是功德无量
2019年4月10日 15:21 | # | 引用
monch 说:
请问 比如获取 最后一篇文章的api怎么设计
首先,要的是文章 api应该这样写,/api/articles/
但是,已经确定了要的是一篇文章,所以不应该以数组的形式返回了吧,但是又不知道最后一篇的id
所以类似这种的api怎么写呢
(不知道ID,然后加了条件,只需要返回单个资源)
2019年4月12日 15:58 | # | 引用
王瑞芳 说:
老师,您有课程吗?在哪里可以看
2019年5月 7日 13:47 | # | 引用
路过看看 说:
引用binger的发言:有个疑问,发生错误了状态码不能为200,应该给出具体状态码,错误放在返回值中,反正都是要解析返回值的,状态码200不是少判断一步状态码么。。。
这里的错误是指的http类型的错误,而不是你的业务逻辑错误,业务逻辑的错误还是需要自行约定code
2019年5月30日 18:09 | # | 引用
高媛 说:
老师,有出系统的前端全栈培训吗,很喜欢老师的文章
2019年5月31日 21:48 | # | 引用
Haven 说:
1.4 复数 URL
既然 URL 是名词,那么应该使用复数,还是单数?
这没有统一的规定,但是常见的操作是读取一个集合,比如GET /articles(读取所有文章),这里明显应该是复数。
为了统一起见,建议都使用复数 URL,比如GET /articles/2要好于GET /article/2。
------------------------------------
其实这里挺难说服我的,在DELETE、PUT、PATCH、GET(获得单条数据)这些接口基本都是操作单条数据的,应使用单数。而只有列表一个接口是多条数据,使用复数。那按照少数服从多数( - _-),应该使用单数才对。
2019年6月20日 17:45 | # | 引用
男儿带吴钩 说:
感觉要客户端去判断数据是应该用post创建还是用put/patch去修改有些麻烦,特别是客户端数据结构比较多的情况下。我个人倾向于一个post包打天下,不管是创建还是更新,都用只用post方法。这样虽然不是那么符合规范,但是实现起来相对比较容易。
2019年6月26日 16:04 | # | 引用
悟天特斯 说:
学习了
3.2部分 第三段有别字
2019年7月 4日 09:35 | # | 引用
小北 说:
比如对一条记录有多种动作怎么做呢?
是:
POST /datas/1?action=reportError
POST /datas/1?action=mark
POST /datas/1?action=assign
还是:
POST /datas/1/reportError
POST /datas/1/mark
POST /datas/1/assign
个人觉得下面这样更清晰,且我不需要在接口函数中判断参数写if else。
2019年7月 5日 07:43 | # | 引用
郑 说:
请问一下如果(网页 ,前后端分离)我想要一周的数据,怎样设计? 是前端处理吗?
2019年8月31日 17:56 | # | 引用
leoskey 说:
引用小北的发言:比如对一条记录有多种动作怎么做呢?
是:
POST /datas/1?action=reportError
POST /datas/1?action=mark
POST /datas/1?action=assign还是:
POST /datas/1/reportError
POST /datas/1/mark
POST /datas/1/assign个人觉得下面这样更清晰,且我不需要在接口函数中判断参数写if else。
看了下 Github 的 star ,采用的是第二种
2019年9月 4日 10:11 | # | 引用
旺旺大馒头 说:
@robinson:
关于下单这个,首先,资源是订单,那么你下单其实是新增一个订单资源,那就是"POST /orders",待发货这些只是订单的一个属性,后续应该是通过"PUT /orders/{orderId}" 去进行更新
2019年10月10日 20:30 | # | 引用
lalio 说:
引用旺旺大馒头的发言:@robinson:
关于下单这个,首先,资源是订单,那么你下单其实是新增一个订单资源,那就是"POST /orders",待发货这些只是订单的一个属性,后续应该是通过"PUT /orders/{orderId}" 去进行更新
这样对后端实现不友好,例如,下单,退订,支付,这三个都是比较大的场景,按照你的理解就是全都有这一个接口去完成了。"PUT /orders/{orderId}"
个人感觉POST /orders/下单 、 POST /orders/退订、 POST /orders/支付,这样是更好的设计,但是这几个场景都是很强的动词语境,没法名词化,不符合RESTFUL了。
2019年11月 4日 16:53 | # | 引用
哈哈 说:
GET /authors/12?categories=2
这种就不算是RESTful风格的了吧
只能说是API了
2019年12月15日 15:09 | # | 引用
mzghm 说:
阮工的文章总是言简意赅,读起来顺畅清晰
2019年12月27日 17:53 | # | 引用
我是一只小小鸟 说:
我也存在和订单类似的问题,比如是用户的启用与禁用,接口该如何设计呢?是PUT /users/{id}/enbale 还是 PUT /users/{id}/status?status_value=enbale,我个人是更倾向于前者的,至少表达清晰,通过接口就能知道是干啥。
另外,还有批量启用和禁用这类的批量操作该如何定义和设计呢?此时用PUT /users/{id}/enbale这个也不合适了。望解答。