restfulApi相关

RESTful 是目前最流行的 API 设计规范,用于 Web 数据接口的设计。

它的大原则容易把握,但是细节不容易做对。本文总结 RESTful 的设计细节,介绍如何设计出易于理解和使用的 API。

restfulApi相关_第1张图片

一、URL 设计

1.1 动词 + 宾语

RESTful 的核心思想就是,客户端发出的数据操作指令都是"动词 + 宾语"的结构。比如,GET /articles这个命令,GET是动词,/articles是宾语。

动词通常就是五种 HTTP 方法,对应 CRUD 操作。

  • GET:读取(Read)
  • POST:新建(Create)
  • PUT:更新(Update)
  • PATCH:更新(Update),通常是部分更新
  • DELETE:删除(Delete)

根据 HTTP 规范,动词一律大写。

1.2 动词的覆盖

有些客户端只能使用GETPOST这两种方法。服务器必须接受POST模拟其他三个方法(PUTPATCHDELETE)。

这时,客户端发出的 HTTP 请求,要加上X-HTTP-Method-Override属性,告诉服务器应该使用哪一个动词,覆盖POST方法。


POST /api/Person/4 HTTP/1.1  
X-HTTP-Method-Override: PUT

上面代码中,X-HTTP-Method-Override指定本次请求的方法是PUT,而不是POST

1.3 宾语必须是名词

宾语就是 API 的 URL,是 HTTP 动词作用的对象。它应该是名词,不能是动词。比如,/articles这个 URL 就是正确的,而下面的 URL 不是名词,所以都是错误的。

  • /getAllCars
  • /createNewCar
  • /deleteAllRedCars

1.4 复数 URL

既然 URL 是名词,那么应该使用复数,还是单数?

这没有统一的规定,但是常见的操作是读取一个集合,比如GET /articles(读取所有文章),这里明显应该是复数。

为了统一起见,建议都使用复数 URL,比如GET /articles/2要好于GET /article/2

1.5 避免多级 URL

常见的情况是,资源需要多级分类,因此很容易写出多级的 URL,比如获取某个作者的某一类文章。


GET /authors/12/categories/2

这种 URL 不利于扩展,语义也不明确,往往要想一会,才能明白含义。

更好的做法是,除了第一级,其他级别都用查询字符串表达。


GET /authors/12?categories=2

下面是另一个例子,查询已发布的文章。你可能会设计成下面的 URL。


GET /articles/published

查询字符串的写法明显更好。


GET /articles?published=true

二、状态码

2.1 状态码必须精确

客户端的每一次请求,服务器都必须给出回应。回应包括 HTTP 状态码和数据两部分。

HTTP 状态码就是一个三位数,分成五个类别。

  • 1xx:相关信息
  • 2xx:操作成功
  • 3xx:重定向
  • 4xx:客户端错误
  • 5xx:服务器错误

这五大类总共包含100多种状态码,覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。

API 不需要1xx状态码,下面介绍其他四类状态码的精确含义。

2.2 2xx 状态码

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"
  }
}

2.3 3xx 状态码

API 用不到301状态码(永久重定向)和302状态码(暂时重定向,307也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API 级别可以不考虑这两种情况。

API 用到的3xx状态码,主要是303 See Other,表示参考另一个 URL。它与302307的含义一样,也是"暂时重定向",区别在于302307用于GET请求,而303用于POSTPUTDELETE请求。收到303以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。下面是一个例子。


HTTP/1.1 303 See Other
Location: /api/orders/12345

2.4 4xx 状态码

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:客户端的请求次数超过限额。

2.5 5xx 状态码

5xx状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。

500 Internal Server Error:客户端请求有效,服务器处理时发生了意外。

503 Service Unavailable:服务器无法处理请求,一般用于网站维护状态。

三、服务器回应

3.1 不要返回纯本文

API 返回的数据格式,不应该是纯文本,而应该是一个 JSON 对象,因为这样才能返回标准的结构化数据。所以,服务器回应的 HTTP 头的Content-Type属性要设为application/json

客户端请求时,也要明确告诉服务器,可以接受 JSON 格式,即请求的 HTTP 头的ACCEPT属性也要设成application/json。下面是一个例子。


GET /orders/2 HTTP/1.1 
Accept: application/json

3.2 发生错误时,不要返回 200 状态码

有一种不恰当的做法是,即使发生错误,也返回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."
  }
}

3.3 提供链接

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" }
  ]}
}

四、参考链接

  • RESTful API Design: 13 Best Practices to Make Your Users Happy, by Florimond Manca
  • API design, by MicroSoft Azure

(完)

文档信息

  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 发表日期: 2018年10月 3日

相关文章

  • 2020.02.23: RDF 和 SPARQL 初探:以维基数据为例

    维基百科有一个姐妹项目,叫做"维基数据"(Wikidata)。你可以从维基百科左侧边栏点进去。

  • 2020.01.14: FFmpeg 视频处理入门教程

    FFmpeg 是视频处理最常用的开源软件。

  • 2019.12.29: Bash 脚本如何创建临时文件:mktemp 命令和 trap 命令教程

    有时,Bash 脚本需要创建临时文件或临时目录。

  • 2019.12.24: 如何撤销 Git 操作?

    Git 版本管理时,往往需要撤销某些操作。

留言(57条)

半卷书 说:

请问 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这个也不合适了。望解答。

你可能感兴趣的:(restfulApi相关)