Restful API如何规范

1,536 阅读17分钟

Restful 接口在平时的开发中非常常见,但是 Restful 接口应该怎么设计各式各样、五花八门。不同的公司(不管大小)甚至可能不同的项目组都不一样。今天从国外的公司,国内的公司、以及自己工作的公司的 Restful接口设计来讲一下自己对设计的理解。

1. 什么是Restful?

REST是具象状态传输(REpresentational State Transfer)的首字母缩写,是分布式超媒体系统的一种架构风格。Roy Fielding于2000年在他的著名论文中首次提出了这一观点。

简单点说就是一种更加优雅架构风格。让使用者更加的舒服。

2. Restful API设计中面临的问题

在开发过程中(以Java项目举例),在搭建好了项目后如何定义API会面临一下几个问题。

2.1 Restful API接口的版本控制

大多数的公司应该都没用版本控制,或者说版本控制比较随便。这也是有原因的:大多时候项目的快速的迭代开发导致API的使用周期可能就是开发到预生产的时间跨度,然后就业务重来了,更不要说几年。这种情况很大情况是有业务导致,重新梳理发现在原有的代码上改造还不如重新写一个接口给你。加版本不存在的,加了版本也没实际意义,因为之前的接口可能再也用不到了(其他项目或者对外也没用到)。

如果有版本管理(以Java Spring 项目为例),版本管理是设置在 Controller 类上面还是放在项目的 server.context-path

@RestController
@RequestMapping("/v1/log")
public class TestController {

    @Async
    @GetMapping("")
    public String log(){
        return null;
    }
}
server.context-path=/v1/aaa

控制的粒度不一样, Controller 上控制单个功能模块的,而 server.context-path 的修改这是整个项目。

实际工作中这两种都用过,我们公司很多业务变化速度开发都快跟不上了,所以加版本与不加版本两者用起来我也没觉得差别在哪。

2.2 Restful API HTTP方法选择

HTTP方法选择,在很多实际使用过程中都没按照HTTP方法的语义来进行选择。大多数时候是怎么方便怎么来,能够实现业务功能就可以,管你什么语义不语义。在看如何选择HTTP方法之前来看一下不同方法的含义是什么,同时Restful API需要支持哪些方法:

方法说明是否幂等
GETGET方法请求一个指定资源的表示形式,使用GET的请求应该只被用于获取数据TRUE
HEADHEAD方法请求一个与GET请求的响应相同的响应,但没有响应体。TRUE
POSTPOST方法用于将实体提交到指定的资源,通常导致在服务器上的状态变化或副作用FALSE
PUTPUT方法用请求有效载荷替换目标资源的所有(替换一个对象,或者创建一个命名的对象)TRUE
DELETEDELETE方法删除指定的资源TRUE
OPTIONSOPTIONS方法用于描述目标资源的通信选项TRUE
PATCHPATCH方法用于对资源应用部分修改FALSE

上表格少了两个方法:CONNECTTRACE 这两个在实际开发中极少用到。同时也不是Restful API 需要支持的

规范:

  • GET与HEAD

    两个方法都是用于获取资源,且只能用于获取资源。区别在于GET有响应体,HEAD没有响应体

  • POST与PUT

    POST用于创建一个新对象,或者提交一个命令,而PUT是替换一个对象,或者创建一个已命名的对象,POST是非幂等的而PUT是幂等

  • PATCH和PUT

    PATH用于更新对象的部分数据,PUT更新对象的全部数据。

根据HTTP方法的不同语义可以知道,不同的情况下Restful接口来选择不同的方法。创建对象就用 POST, 部分更新用PATCH,获取用对象的信息就用GET。

实际过程中遇到过这样的一个问题:根据批量的ID获取对象的信息入这样的请求:

GET http://api.mxsm.com/accounts?ids=1,2,....,n

这里ids数据可能会有上千个,最终导致的问题是URI的长度超过规定的最大长度,导致部分数据传不到后台。

实际过程我们为了解决这个问题会把GET方式改为POST方式ID放在payload中

POST http://api.mxsm.com/accounts
{
"ids":[1,...,n]
}

这这方式又违背了HTTP 方法的相关语义。

2.3 接口状态码是否复用HTTP状态码

对于状态码的问题绝大多数的Restful接口设计都是请求的接口不论后端是处理正确还是处理错误 HTTP的状态码:200, 返回的数据根据格式如下:

HTTP Status Code : 200
{
  "code": "00000",
  "data": 0,
  "msg": "string",
  "traceId": "string"
}

根据不同的错误code的编码不同, data可能存在或者不存在(取决于是否正确返回),msg有没有取决于处理正确与否或者看自己的定义。

另一种方式就是把接口状态码与HTTP状态码相结合:

HTTP Status Code说明
2XX成功的执行。 方法执行可以通过几种方式成功。 这个状态代码指定了它成功的方式。
4XX这些问题通常与请求、请求中的数据、无效的身份验证或授权等有关。 在大多数情况下,客户端可以修改他们的请求并重新提交。
5XX服务器错误:由于站点中断或软件缺陷,服务器无法执行该方法。 5xx范围状态码不应用于验证或逻辑错误处理。

HTTP状态码说明:

接口状态码与HTTP状态码相结合这种方式在公司中比较少。

工作中有这种使用方式,最终大家觉得这样太麻烦了(前端、后台觉得我还需要处理这么多HTTP状态码不太想做)。最终没有能够坚持的执行下去。

2.4 URI的命名规则

URI的设计规则比前面几个都复杂,前面的还有一个固定可参照的规范来进行参照。这个没办法参照,特别是在项目紧急的时候,开发都是本着能用就行的方式进行开发,至于到底符不符合规范都没用那么的重要。

GET https://blog.ljbmxsm.com/blog/1
GET https://blog.ljbmxsm.com/blogs/1
GET https://blog.ljbmxsm.com/bg/1
GET https://blog.ljbmxsm.com/getBlog/1

如上面的例子,我就是想获取一下一篇ID为1的blog内容,就可能被写出这么多五花八门的URI。这里到底需要遵循一个什么样的规范和一个什么标准。后面参照其他比较大的公司的Restful接口设计给出总结。

3. 大公司如何设计Restful API

下面来分析一下国内和国外的知名公司看一下大公司是如何设计 Restful API ,不同公司有什么不同,有哪些是相同的。国内选择 企鹅(微信开放平台)、阿里巴巴(钉钉开放平台),国外的选择 微软、paypal 作为对照。

3.1 微信开放平台

例子地址:developers.weixin.qq.com/doc/oplatfo…

image.png

image.png

分析说明:

  • 微信开放平台的接口没有看到URI中有版本的控制(可以理解为提供对外的接口没有做过版本升级至少接口层面看上去是)
  • 对于不同的数据选择不同的HTTP方法进行数据获取
  • 当后台验证或者获取失败的情况下,微信开放平台是通过 errcode 方式来返回错误信息, HTTP状态码是: 200
  • URI的请求语义都是不带有动词的,都是名称。单词分隔符用的 -(横杠非下划线) ,查询的条件使用的是下划线,返回的数据如果是多个单词也是用下划线的方式进行分割。

3.2 钉钉开放平台

钉钉接口约束文档:open.dingtalk.com/document/or…

image.png

分析说明:

  • URI存在 version 的版本管控,API有不同的版本
  • 对于不同的数据选择不同的HTTP方法进行数据获取
  • 当后台验证或者获取失败的情况下,接口错误码和HTTP状态码相结合(错误码说明:open.dingtalk.com/document/or…)
  • URI的请求语义都是不带有动词的,都是名称。路径中的名称使用的是驼峰的风格,查询条件也是使用的驼峰风格。(这里感觉是和开发语言相关,阿里主打Java)

3.3 微软

文档地址:docs.microsoft.com/en-us/rest/… ,这个是在微软Restful接口网站找了一个例子:

image.png

分析说明:

  • 在路径中没有存在版本,但是在URI的查询条件后面存在一个api版本的查询条件
  • 对于不同的数据选择不同的HTTP方法进行数据获取
  • 接口错误码和HTTP状态码相结合
  • URI的请求语义都是不带有动词的,都是名称。单词分隔符用的 -(横杠非下划线) ,查询的条件使用的是横杠,返回的数据使用的是驼峰。

参考网站:

3.4 PayPal

PayPal文档地址:developer.paypal.com/api/rest/

curl -v POST https://api-m.sandbox.paypal.com/v1/oauth2/token \
  -H "Accept: application/json" \
  -H "Accept-Language: en_US" \
  -u "CLIENT_ID:SECRET" \
  -d "grant_type=client_credentials"

返回值:

{
    "scope": "https://uri.paypal.com/services/invoicing https://uri.paypal.com/services/disputes/read-buyer https://uri.paypal.com/services/payments/realtimepayment https://uri.paypal.com/services/disputes/update-seller https://uri.paypal.com/services/payments/payment/authcapture openid https://uri.paypal.com/services/disputes/read-seller https://uri.paypal.com/services/payments/refund https://api-m.paypal.com/v1/vault/credit-card https://api-m.paypal.com/v1/payments/.* https://uri.paypal.com/payments/payouts https://api-m.paypal.com/v1/vault/credit-card/.* https://uri.paypal.com/services/subscriptions https://uri.paypal.com/services/applications/webhooks",
    "access_token": "A21AAFEpH4PsADK7qSS7pSRsgzfENtu-Q1ysgEDVDESseMHBYXVJYE8ovjj68elIDy8nF26AwPhfXTIeWAZHSLIsQkSYz9ifg",
    "token_type": "Bearer",
    "app_id": "APP-80W284485P519543T",
    "expires_in": 31668,
    "nonce": "2020-04-03T15:35:36ZaYZlGvEkV4yVSz8g6bAKFoGSEzuy3CQcz3ljhibkOHg"
}

分析说明:

  • URI存在 version 的版本管控,API有不同的版本
  • 对于不同的数据选择不同的HTTP方法进行数据获取
  • 接口错误返回和HTTP状态码相关联(developer.paypal.com/api/rest/re…)
  • URI的请求语义都是不带有动词的,都是名称。单词分隔符用的 -(横杠非下划线) ,查询的条件使用的是下划线,返回的数据如果是多个单词也是用下划线的方式进行分割

参考网站:


通过上面的四个不同的公司对外开放的API可以总结出来以下几个共同点:

  • Restful API设计需要考虑版本,除了微信没有看到明确的版本控制,微软放在查询条件后面
  • 根据语义不同使用不同HTTP不同的请求方式
  • 接口的错误码和HTTP状态码相结合,微信是通过code表示所有的状态,没有和HTTP状态码进行关联
  • URI路径中都不带有动词,都是名称。路径中名称有使用驼峰(钉钉),也有使用 -(横杠) 作为分隔符

4. Restful API如何设计

从上面的案例的对比可以看出来,不同公司有着不同的设计风格。不同的风格中也有着共同点。那我们就从这些相同的和不同的,设计一个符合自己工作或者学习过程中的Restful API 规范。

4.1 Restful API 版本管理

API的版本管理还是需要,版本管理最好加载 Controller上面或者对应的方法上面。

@RestController
@RequestMapping("/v1/log")
public class TestController {

    @Async
    @GetMapping("")
    public String log(){
        return null;
    }
}

@RestController
@RequestMapping("")
public class TestController {

    @GetMapping("/v1/log")
    public String log(){
        return null;
    }
}

好处:

  • 更细粒度的管理接口版本,并不是每一次API升级都是整个项目的API。
  • 对应已存在的项目对于某些接口加上版本管理页更加方便。

4.2 不同的语义接口要使用不同的HTTP方法

HTTP Method说明是否要幂等
GET主要用于从服务端获取对象信息(仅仅只是使用获取用户对象)
POST创建对象
PUT更新对象(更新对象所有的内容)
DELETE删除一个对象
PATCH更新对象(部分更新)
OPTIONS获取服务器支持的HTTP请求方法,用来检查服务器的性能

CRUD 选择不同的 HTTP Method ,不应该都使用一种方式来操作所有,比如增删改查都用POST的方式(刚毕业那会我就这么干过),这样做是可以但是不符合语义。

前面也提到过,如果批量请求对象的信息,可能会导致URL长度超过,这种情况应该通过分批次请求。而不是一次性请求。在业务上来说一次性获取这么多数据本身就是不合理。

4.3 HTTP状态码和接口错误码相结合

大多数情况下HTTP状态码和接口错误码是分开的,并不会有关联,这也是大多公司的做法。原因也很多,前后端处理起来都麻烦。但是很多监控都可以直接通过检测HTTP状态码来统计。但是如果不管后台抛什么错误都以 HTTP code 200 然后根据接口错误码来判断就会很麻烦。从上面的几个大公司的设计来看,HTTP状态码和接口错误码相结合 是一个不错的选择

公司已存在的项目可以保持原有的,新建的项目可以用这种形式。

结构:

HTTP status code 400
{
    "errorCode":"4000",
    "errorMessage":"错误"
}

errorCode 错误码定义为业务的错误码, errorMessage 是具体的错误信息。

状态码状态码英文名称中文描述
200OK请求成功。一般用于GET与POST请求
201Created已创建。成功请求并创建了新的资源
202Accepted已接受。已经接受请求,但未处理完成
203Non-Authoritative Information非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本
204No Content无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档
205Reset Content重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域
206Partial Content部分内容。服务器成功处理了部分GET请求
302Found临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
303See Other查看其它地址。与301类似。使用GET和POST请求查看
304Not Modified未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
305Use Proxy使用代理。所请求的资源必须通过代理访问
306Unused已经被废弃的HTTP状态码
307Temporary Redirect临时重定向。与302类似。使用GET请求重定向
400Bad Request客户端请求的语法错误,服务器无法理解
401Unauthorized请求要求用户的身份认证
402Payment Required保留,将来使用
403Forbidden服务器理解请求客户端的请求,但是拒绝执行此请求
404Not Found服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面
405Method Not Allowed客户端请求中的方法被禁止
406Not Acceptable服务器无法根据客户端请求的内容特性完成请求
407Proxy Authentication Required请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权
408Request Time-out服务器等待客户端发送的请求时间过长,超时
409Conflict服务器完成客户端的PUT请求是可能返回此代码,服务器处理请求时发生了冲突
410Gone客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置
411Length Required服务器无法处理客户端发送的不带Content-Length的请求信息
412Precondition Failed客户端请求信息的先决条件错误
413Request Entity Too Large由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息
414Request-URI Too Large请求的URI过长(URI通常为网址),服务器无法处理
415Unsupported Media Type服务器无法处理请求附带的媒体格式
416Requested range not satisfiable客户端请求的范围无效
417Expectation Failed服务器无法满足Expect的请求头信息
500Internal Server Error服务器内部错误,无法完成请求
501Not Implemented服务器不支持请求的功能,无法完成请求
502Bad Gateway充当网关或代理的服务器,从远端服务器接收到了一个无效的请求
503Service Unavailable由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中
504Gateway Time-out充当网关或代理的服务器,未及时从远端服务器获取请求
505HTTP Version not supported服务器不支持请求的HTTP协议的版本,无法完成处理

4.4 URI设计

4.4.1 一类资源
#资源集合
/accounts
#单个资源
/account/{accountId}
4.4.2 单复数统一
#资源集合
/accounts
#单个资源
/account/{accountId}

#统一
/accounts
/accounts/{accountId}
4.4.3 使用名词而不是动词

使用HTTP方法来表达动作而不是通过URI的中的动词来表达

#获取集合
GET /accounts
#获取集合--这里就是一个反例
GET /get/accounts/
4.4.4 支持复杂查询
OperatorDescriptionExample
Comparison Operators
eqEqualcity eq 'Redmond'
neNot equalcity ne 'London'
gtGreater thanprice gt 20
geGreater than or equalprice ge 10
ltLess thanprice lt 20
leLess than or equalprice le 100
Logical Operators
andLogical andprice le 200 and price gt 3.5
orLogical orprice le 3.5 or price gt 200
notLogical negationnot price le 3.5
Grouping Operators
( )Precedence grouping(priority eq 1 or city eq 'Redmond') and price gt 100

上面表格来源微软api指导:github.com/microsoft/a…

GET https://api.contoso.com/v1.0/products?$filter=name eq 'Milk'

这种复杂的查询可以实现也可以不实现。

4.5 返回JSON数据格式

{
  "error": {
    "code": "BadArgument",
    "message": "Previous passwords may not be reused",
    "target": "password",
    "innererror": {
      "code": "PasswordError",
      "innererror": {
        "code": "PasswordDoesNotMeetPolicy",
        "minLength": "6",
        "maxLength": "64",
        "characterTypes": ["lowerCase","upperCase","number","symbol"],
        "minDistinctCharacterTypes": "2",
        "innererror": {
          "code": "PasswordReuseNotAllowed"
        }
      }
    }
  }
}

{
  "errorCode": “”,
    "errorMessage":"",
    "data":{
        
    }
}

返回的格式尽量简单,能够表达意思就好。

5. 总结

  • 对于公司应该定制一个统一的规范,特别是有对外暴露自己的接口。需要参照一定的规范进行设计(对内、对外最好统一规范)

  • HTTP状态码和接口错误码相结合是有必要的,这样能够更加清楚的表达接口的状态(至少规则指定后项目要遵守)

  • URI的通用规则应该统一,这样整个URI看起来就很整洁,可读性更强

参考文档: