RESTful API 最佳实践

作者: 阮一峰

日期: 2018年10月 3日

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

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

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

四、参考链接

(完)

留言(62条)

请问 RESTful API 对SEO友好吗?由其是像 GET /authors/12?categories=2这种的url

发现阮老师博客head也新加上了下border。我之前自己加过一段时间,后来觉得还是太丑了,哈哈

github的api似乎也是倾向于使用多级而不是查询字符串,这么说也不符合最佳实践吗?

上一篇REST还有印象hhh

大佬来写果然深度不一样,也欢迎大家去看看我总结的 restful api 规范

https://godruoyi.com/posts/resetful-api-design-specifications

请教一下大家,如果遇到动词不在常见的几种之中,甚至是需要自定义的动词,怎么做比较合理?

422说的有点含糊,换个意思说,其实最常用的场景是服务器端表单验证失败

引用t的发言:

请教一下大家,如果遇到动词不在常见的几种之中,甚至是需要自定义的动词,怎么做比较合理?

这个动词是HTTP固定的吧,其实更多的动词场景,我理解都可以区分成几种,只要是获取信息,都可以用GET,如果是在基本信息表增加记录,就是POST,其他只要是修改,或者是修改关系表这种情况,应该都是UPDATE,update和put其实是有差别的。如果要update的行为很多,我会在后面增加?type= 这类参数,如果要是特别直接的动作,比如upload这种,直接放在最高的级别也ok啊。 abc.com/upload

3.1 不要返回纯本文
标题打错啦

请问对于登录操作,可以用restful api的格式吗?如果可以,对应的资源是什么呢?

引用etworker的发言:

请问对于登录操作,可以用restful api的格式吗?如果可以,对应的资源是什么呢?

POST /session

204 No Content 应该是指没有需要返回给客户端的内容,而不是服务端的内容已经不存在

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204

前几天看完您的js全栈,正好在查REST,最近您就出了,真的厉害!

我们的实践: 400 用于表示客户端传参错误(或者不完全), 200 有可能也是不正常的响应(当然不能算是错误), 比如用户名或者密码不匹配.

通俗易懂 深受启发

关于restful风格和rpc风格的api设计和公司同事有过争论,感觉是主义之争,不会有什么结果。不过关于rest风格,在实际应用中,也遇到过难处理的问题,比如,client验证用户名或者电话是否存在,就不知如何设计怎么好,最后“强行”设计成:GET /users/checking(validating)?username=xx,反倒是,rpc风格,GET /users.check?username=xx是否表达力更强一些?再如,某个操作导致状态更新,总结下,就是对于有很强的“动作”在内的api,应该如何用rest风格设计?这个问题困扰我很久了,望阮老师解惑,先在这里谢过了。

多个资源的关联关系的变更,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

公司进行nessus扫描时,报告web server只允许使用GET和POST,不允许使用其他方法。方法覆写也是禁止的。请问,这种冲突,应该如何解决?

@robinson:

可以看阮老师的这篇文章中7、误区,里面有讲述服务的设计。
http://www.ruanyifeng.com/blog/2011/09/restful.html

老师,请问,如果跨域前端能取得到http错误码吗,我们公司前端说跨域的时候只能取到200其他的取不到,所有如果真的取不到,那请问是不是比如:404的时候也要返回200,然后把错误信息和404错误码放在数据体里面。是吗

关于1.5节,仅仅举了GET命令的例子,但是对二级资源做POST/PUT/DELETE的时候,是否还可以使用查询字符串表达?

对于查询字符串,我们在应用的范式是当定位某种资源时,用多级地址,但当定义response如何返回时,用查询字符串,比如返回是否是paginate的,最大返回多少

引用半卷书的发言:

请问 RESTful API 对SEO友好吗?由其是像 GET /authors/12?categories=2这种的url

这个没关系吧,看你的页面是服务端渲染还是前端渲染吧

@Joshua

那篇文章,我也拜读过,但还是有疑惑的,我们是可以向都是名词化靠拢,但这个世界难道都可以“资源化”吗?比如我遇到的问题,检查用户是否存在,难道一定要按用户名查询用户?如果返回了用户,那就是存在?同样的情况还有:验证验证码是否正确。还有订单的情景,我下单后订单状态成为“待发货”,但如果按照“资源化”的思路,应该如何设计呢?“PUT /orders/{id}?action=下单”?还是PUT /orders/{id}?status=代发货?或者/orders/{id}/status/待发货?我感觉后面的这种情况更严重,这样封装性很差,把逻辑交给了下游,有为了rest而rest之嫌,如果是指定action的情况,那么也比较糟糕,难道我们对订单的接口只有四个?其余的都只能通过参数表达?后端实现也会成为一锅粥。还望各位大牛解惑

感觉没看懂呀。。。

勘误下:
3.2里 “Invalid payoad.”
是payload吧,单词拼写错误

引用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操作

PUT /user/${user_id}.join-to/team/${team_id}
是否可以

怎么看都觉得少了点什么,也许功能测试都没有什么问题,各种cornner都要测到,但是性能测试可否详细谈一下?locust?

比如获取某个作者的某一类文章。

这个例子写的示例语义上不太好,返回的资源其实是文章,那么应该表述为

GET /articles?authorId=12&categoryId=2

本来就没有层级关系

另外,某类的所有文章,某作者的所有文章
GET /category/2/articles
GET /author/12/articles

有个疑问,发生错误了状态码不能为200,应该给出具体状态码,错误放在返回值中,反正都是要解析返回值的,状态码200不是少判断一步状态码么。。。

为什么没有502

引用李的发言:

老师,请问,如果跨域前端能取得到http错误码吗,我们公司前端说跨域的时候只能取到200其他的取不到,所有如果真的取不到,那请问是不是比如:404的时候也要返回200,然后把错误信息和404错误码放在数据体里面。是吗

怎么可能取不到,只要是基于http协议的都可以。只是他们没有这么做,要么是前端技术low,对于这种你就把这篇文章丢给他即可,其他什么都不要说。

引用郑诚 的发言:

为什么没有502

文章都说的很清楚,对于服务端异常,一般不会透露过多的信息:
5xx状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。

当然你也要把更多的异常信息往外抛,看你了,只是不建议。

引用binger的发言:

有个疑问,发生错误了状态码不能为200,应该给出具体状态码,错误放在返回值中,反正都是要解析返回值的,状态码200不是少判断一步状态码么。。。

你可以返回实际的状态码,比如你现在要返回的HTTP状态码是404,那么返回的JSON中状态码也可以用404,其他也是类似的。

@Shuo Wang:

你这种就不应该放在一起,分开写

引用fengchang的发言:

204 No Content 应该是指没有需要返回给客户端的内容,而不是服务端的内容已经不存在

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204

删除了是没有啊,表示这个资源已经不存在,用204没毛病。其实没必要太钻牛角尖,能基本表示清楚就可以了。

api呈现给用户可以继承swgger。哈哈哈哈......手写API文档的岁月一去不复返

@robinson:

你把“验证存在性”理解为“尝试获取”就好办了,直接GET /users/你想获取的用户名 ,不存在就直接返回不存在就是了。

老阮的你可真是功德无量

请问 比如获取 最后一篇文章的api怎么设计

首先,要的是文章 api应该这样写,/api/articles/
但是,已经确定了要的是一篇文章,所以不应该以数组的形式返回了吧,但是又不知道最后一篇的id


所以类似这种的api怎么写呢
(不知道ID,然后加了条件,只需要返回单个资源)

老师,您有课程吗?在哪里可以看

引用binger的发言:

有个疑问,发生错误了状态码不能为200,应该给出具体状态码,错误放在返回值中,反正都是要解析返回值的,状态码200不是少判断一步状态码么。。。

这里的错误是指的http类型的错误,而不是你的业务逻辑错误,业务逻辑的错误还是需要自行约定code

老师,有出系统的前端全栈培训吗,很喜欢老师的文章

1.4 复数 URL

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

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

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

------------------------------------

其实这里挺难说服我的,在DELETE、PUT、PATCH、GET(获得单条数据)这些接口基本都是操作单条数据的,应使用单数。而只有列表一个接口是多条数据,使用复数。那按照少数服从多数( - _-),应该使用单数才对。

感觉要客户端去判断数据是应该用post创建还是用put/patch去修改有些麻烦,特别是客户端数据结构比较多的情况下。我个人倾向于一个post包打天下,不管是创建还是更新,都用只用post方法。这样虽然不是那么符合规范,但是实现起来相对比较容易。

学习了
3.2部分 第三段有别字

比如对一条记录有多种动作怎么做呢?

是:

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。

请问一下如果(网页 ,前后端分离)我想要一周的数据,怎样设计? 是前端处理吗?

引用小北的发言:

比如对一条记录有多种动作怎么做呢?

是:

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 ,采用的是第二种

@robinson:

关于下单这个,首先,资源是订单,那么你下单其实是新增一个订单资源,那就是"POST /orders",待发货这些只是订单的一个属性,后续应该是通过"PUT /orders/{orderId}" 去进行更新

引用旺旺大馒头的发言:

@robinson:

关于下单这个,首先,资源是订单,那么你下单其实是新增一个订单资源,那就是"POST /orders",待发货这些只是订单的一个属性,后续应该是通过"PUT /orders/{orderId}" 去进行更新

这样对后端实现不友好,例如,下单,退订,支付,这三个都是比较大的场景,按照你的理解就是全都有这一个接口去完成了。"PUT /orders/{orderId}"

个人感觉POST /orders/下单 、 POST /orders/退订、 POST /orders/支付,这样是更好的设计,但是这几个场景都是很强的动词语境,没法名词化,不符合RESTFUL了。

GET /authors/12?categories=2
这种就不算是RESTful风格的了吧
只能说是API了

阮工的文章总是言简意赅,读起来顺畅清晰

我也存在和订单类似的问题,比如是用户的启用与禁用,接口该如何设计呢?是PUT /users/{id}/enbale 还是 PUT /users/{id}/status?status_value=enbale,我个人是更倾向于前者的,至少表达清晰,通过接口就能知道是干啥。
另外,还有批量启用和禁用这类的批量操作该如何定义和设计呢?此时用PUT /users/{id}/enbale这个也不合适了。望解答。

GET /authors/12?categories=2
不伦不类

引用robinson的发言:

@Joshua

那篇文章,我也拜读过,但还是有疑惑的,我们是可以向都是名词化靠拢,但这个世界难道都可以“资源化”吗?比如我遇到的问题,检查用户是否存在,难道一定要按用户名查询用户?如果返回了用户,那就是存在?同样的情况还有:验证验证码是否正确。还有订单的情景,我下单后订单状态成为“待发货”,但如果按照“资源化”的思路,应该如何设计呢?“PUT /orders/{id}?action=下单”?还是PUT /orders/{id}?status=代发货?或者/orders/{id}/status/待发货?我感觉后面的这种情况更严重,这样封装性很差,把逻辑交给了下游,有为了rest而rest之嫌,如果是指定action的情况,那么也比较糟糕,难道我们对订单的接口只有四个?其余的都只能通过参数表达?后端实现也会成为一锅粥。还望各位大牛解惑

这也是很困扰我的一个问题,请问谁有最佳实践吗?

引用DimonHo的发言:

这也是很困扰我的一个问题,请问谁有最佳实践吗?

可以参考下面的方式
https://geemus.gitbooks.io/http-api-design/content/en/requests/actions.html

Actions
Prefer endpoint configurations that don’t require special actions. In cases where actions are needed, clearly dileanate them with the actions prefix:
/resources/:resource/actions/:action
e.g. to stop a particular run:
/runs/{run_id}/actions/stop
Actions on collections should also be minimized. Where needed, they should use a top-level actions dilenation to avoid namespace conflicts and clearly show the scope of action:
/actions/:action/resources
e.g. to restart all servers:
/actions/restart/servers

阮老师好,关注您的博客有一段时间了,今天参考您的关于REST的博客摘取一些内容与同事讨论,特来此说明,感谢阮老师的分享。

1.5 避免多级 URL
常见的情况是,资源需要多级分类,因此很容易写出多级的 URL,比如获取某个作者的某一类文章。
GET /authors/12/categories/2

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

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

GET /authors/12?categories=2

----------------------------------------


这个正例举的不恰当,从url上完全看不出来要获取的是“文章”,articles是资源,所以第一级应该是articles而不是authers。正确的写法应该是/articles?authers=12&categories=2

看了这篇博客,我看并没有提出使用业务错误码。类似下面的返回
{
"code":"", // int, 表示业务错误码
"message":"", // 错误消息
"data":"" // 具体数据
}
请问我们在开发rest接口的时候,需要提供业务错误码不?如果不提供,那么针对openapi这种(既不一定是前端调用的接口,可能是服务间调用的接口)。这时候是不是业务错误码有一定的用处呢?

感觉“某个作者的某一类文章。”应该用/articles?author=:author&category=:category更好吧

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接