RESTFul API 设计规范

怎甘沉沦 提交于 2020-03-02 04:58:35

0. API设计满足关键点

  1. API应当基于 web 标准来设计
  2. API应当对开发者友好并且便于在浏览器地址栏中浏览和探索
  3. API应当是简单、直观和一致的,使它用起来方便和舒适
  4. API应当是高效的,同时要维持和其他需求之间的平衡

1. API命名

1.1 根地址

好的RESTful API要基于HTTPS来发布 API规模不大时,在域名后面增加 api 目录,如:https://www.trawe.cn/api/ API规模很大时,使用以api开头的二级域名,如:https://api.trawe.cn/

1.2 版本问题

  1. 新版本尽量对旧版本作兼容

  2. 版本信息放在URL中

https://api.trawe.cn/v1.2/users/123 
  1. 协议报文中增加version字段
{
    version: "1.0",
    .... ....
}
  1. HTTP Header中增加版本信息
使用已的HTTPHeader:Accept Header:Accept: application/json+v1.2
自定义 Header:             X-Api-Version: 1.2

1.3 端点设计原则

1) 命名

  1. CRUD操作一律使用名词,不使用动词
  2. url一律使用小写字母
  3. url命名方式不使用 camel方式,采用 - 连接两个单词,如:app-setups,而不是appSetups
  4. 请求参数命名方式使用 下划线 “_” 连接两个单词(Javascript规范),如:user_name,而不是userName或user-name
  5. 在url中不要出现 get / add / delete / put / modify / update 等动词,使用HTTP Method来替代

2) 无论对于单个资源还是集合,名词都使用复数形式,这样便于风格的统一

GET  /tickets      # 获取 tickets 列表
GET  /tickets/12     # 获取一个单独的 ticket
POST /tickets        # 创建一个新的 ticket
PUT  /tickets/12         # 更新 ticket #12
PATCH /tickets/12     # 部分更新 ticket #12
DELETE /tickets/12   #  删除 ticket #12
GET /tickets/12/messages     #  获取ticket #12下的消息列表
GET /tickets/12/messages/5     #  获取ticket #12下的编号为5的消息
POST /tickets/12/messages     #  为ticket #12创建一个新消息
PUT /tickets/12/messages/5     #  更新ticket #12下的编号为5的消息
PATCH /tickets/12/messages/5     #  部分更新ticket #12下的编号为5的消息
DELETE /tickets/12/messages/5    #  删除ticket #12下的编号为5的消息

3) 对于非CRUD的操作 有很非CRUD服务,可以把这些服务看成资源,计算的结果是资源的presentation,按服务属性选择合适的HTTP方法。

1. 重新构造这个Action,使得它像一个资源的操作。
    这种方法在Action不包含参数的情况下可以奏效。例如一个有效的action可以映射成布尔类型field,并且可以通过PATCH更新资源。
2. 利用RESTful原则像处理子资源一样处理它。
    例如:Github的API让你通过PUT /gists/:id/star 来 star a gist ,而通过DELETE /gists/:id/star来进行 unstar 。
3. 有时候你实在是没有办法将Action映射到任何有意义的RESTful结构。
    例如:多资源搜索没办法真正地映射到任何一个资源接入点。这种情况,/search 将非常有意义,虽然它不是一个名词,但是这样做没有问题,只需要从API消费者的角度做正确的事,并确保所做的一切都用文档清晰记录下来了即可。

4) 查询过滤 过滤: 对每一个字段使用一个唯一查询参数,就可以实现过滤。 例如: 当通过 /tickets 终端来请求一个票据列表时,我们需要增加一些限定来查询那些在售的票。可以使用 GET /tickets?state=open 这样的请求来实现。这里“state”是一个实现了过滤功能的查询参数。

对于常用的查询,有以下两种处理:

  1. 可以单独将查询包装为一个独立的API 如:GET /trades?status=closed&sort=created,desc 可以包装GET /trades/recently-closed
  2. 查询结果标签化 将经常使用的、复杂的查询标签化,降低维护成本。如:GET /trades?status=closed&sort=created,desc 可以标签化为 GET /trades#recently-closed

排序: 跟过滤类似,使用排序参数字段来描述排序的规则。 为适应复杂排序需求,让排序参数采取逗号分隔的字段列表的形式,每一个字段前都可能有一个负号来表示按降序排序。 例如:

排序字段前面的 +表示升序   -表示降序  默认为升序
GET /tickets?sort=-priority  # 获取票据列表,按优先级字段降序排序
GET /tickets?sort=-priority,created_at  # 获取票据列表,按“priority”字段降序排序。在一个特定的优先级内,较早的票排在前面。

5) 减少层级深度 /在url中表达层级,用于按实体关联关系进行对象导航,一般根据id导航。 过深的导航容易导致url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4,尽量使用查询参数代替路径中的实体导航,如GET /animals?zoo=1&area=3

6) 限制返回哪些字段 使用一个字段查询参数,它包含一个 用逗号隔开的字段列表。例如,下列请求获得的信息将刚刚足够展示一个在售票的有序列表: GET /tickets?fields=id,subject,customer_name,updated_at&state=open&sort=-updated_at

7) 关于返回结果 通常情况下只返回JSON格式结果。 返回结果支持gzip压缩:进行gzip压缩的数据可节省50%以上的带宽 对于需要返回不同格式资源的情况:

  1. 在HTTP Header中指定格式 如:Accept:application/xml;q=0.6,application/atom+xml;q=1.0
  2. URL后缀增加扩展名: 如:/users/1.xml

8) 分页 常用的分页字段如下:

limit=10:指定返回记录的数量
offset=10:指定返回记录的开始位置。
page=2&per_page=100:指定第几页,以及每页的记录数。

9) 缓存 ETag: 当产生一个请求时, 包含一个HTTP 头,ETag会在里面置入一个和表达内容对应的哈希值或校验值。这个值应当跟随表达内容的变化而变化。现在,如果一个入站HTTP请求包含了一个If-None-Match头和一个匹配的ETag值,API应当返回一个304未修改状态码,而不是返回请求的资源。 Last-Modified: 基本上像ETag那样工作,不同的是它使用时间戳。在响应头中,Last-Modified包含了一个RFC 1123格式的时间戳,它使用If-Modified-Since来进行验证。注意,HTTP规范已经有了 3 种不同的可接受的日期格式 ,服务器应当准备好接收其中的任何一种。

2. HTTP方法

常用:

GET (选择):从服务器上获取一个具体的资源或者一个资源列表。
POST (创建): 在服务器上创建一个新的资源。
PUT (更新):以整体的方式更新服务器上的一个资源。
PATCH (更新):只更新服务器上一个资源的一个属性。
DELETE (删除):删除服务器上的一个资源。

不常用:

HEAD : 获取一个资源的元数据,如数据的哈希值或最后的更新时间。
OPTIONS:获取客户端能对资源做什么操作的信息。

3. 数据报文

  1. 统一报文格式
请求使用JSON格式时:
{
    method: 'testMethod',       // 请求方法名称
    version: '1.0',                     // 接口版本
    token: '0X十六进制',         //  Token
    sign_type: 'MD5',              //  签名算法
    sign: '0X十六进制',            //  签名
    timestamp: '12345678'    // 请求的时间戳
    
}

响应:
{
    code: 0,                        // 返回码。 -1:失败  0:成功  其它:具体业务代码
    message: '处理成功',  // 返回码描述信息
    sign: '0X十六进制',      // 签名
    ...                                   // 业务数据
}

响应数据不作多余包装,如下为错误示例:

{
    code: 0,
    data: {userName: "用户名"} // 这里面的data没有业务含意,仅仅是为了包装,所以应该去掉
}

需要进行包装的情况:

  1. 使用JSONP进行跨域请求
  2. 当客户端没有能力处理HTTP头信息时
  1. 对于日期类型的字段处理
  1. 一种方式为:转为一定格式的字符串,如 yyyy-MM-dd HH:mm:ss.SSS
  2. 另一种方式为:转为长整型数字时间戳

4. 安全

  1. 使用Https传输数据
  2. 使用Token(如JWT)来标识用户状态并设置失效时间
  3. Token失效后客户端自动重新登录获取新的Token
  4. 发送请求时对参数按ASCII排序计算签名(Hash算法或对称加密算法,可以所有接口统一密钥,也可以一个接口一个密钥),接收到请求后先验证签名
  5. 每个端(Android、iOS、微信服务号、Web网站)生成一个AppKey
  6. 不设置密码,登录时使用手机+验证码方式登录

5. 文档

  1. 文档须提供从请求到响应整个循环的示例;
  2. 请求应该是可粘贴的例子,要么是可以贴到浏览器的链接,要么是可以贴到终端里的curl示例 ;
  3. 一旦发布一个公开的API,必须承诺 在没有通知的前提下,不会更改API的功能
  4. 对于外部可见API的更新,文档必须包含任何将废弃的API的时间表和详情;

6. HTTP状态代码

HTTP定义了一套可以从API返回的有意义的状态代码。 这些代码能够用来帮助API使用者对不同的响应做出相应处理。

200 OK (成功)  -  对一次成功的GET, PUT, PATCH 或 DELETE的响应。也能够用于一次未产生创建活动的POST
201 Created (已创建)  -  对一次导致创建活动的POST的响应。 同时结合使用一个位置头信息指向新资源的位置
204 No Content (没有内容) - 对一次没有返回主体信息(像一次DELETE请求)的请求的响应
304 Not Modified (未修改) - 当使用HTTP缓存头信息时使用304
400 Bad Request (错误的请求) - 请求是畸形的, 比如无法解析请求体
401 Unauthorized (未授权) - 当没有提供或提供了无效认证细节时。如果从浏览器使用API,也可以用来触发弹出一次认证请求
403 Forbidden (禁止访问) - 当认证成功但是认证用户无权访问该资源时
404 Not Found (未找到) - 当一个不存在的资源被请求时
405 Method Not Allowed (方法被禁止) - 当一个对认证用户禁止的HTTP方法被请求时
410 Gone (已删除) - 表示资源在终端不再可用。当访问老版本API时,作为一个通用响应很有用
415 Unsupported Media Type (不支持的媒体类型) - 如果请求中包含了不正确的内容类型
422 Unprocessable Entity (无法处理的实体) - 出现验证错误时使用
429 Too Many Requests (请求过多) - 当请求由于访问速率限制而被拒绝时

7. 错误处理

原则

  1. 不要发生了错误但给2xx响应,客户端可能会缓存成功的http请求;
  2. 正确设置http状态码,不要自定义;
  3. Response body 提供 1) 错误的代码(日志/问题追查);2) 错误的描述文本(展示给用户)。

API 可能抛出两类异常:业务异常和非业务异常。 业务异常由自己的业务代码抛出,表示一个用例的前置条件不满足、业务规则冲突等,比如参数校验不通过、权限校验失败。 非业务类异常表示不在预期内的问题,通常由类库、框架抛出,或由于自己的代码逻辑错误导致,比如数据库连接失败、空指针异常、除0错误等等。

业务类异常必须提供2种信息:

  1. 如果抛出该类异常,HTTP 响应状态码应该设成什么;
  2. 异常的文本描述;

在Controller层使用统一的异常拦截器:

  1. 设置 HTTP 响应状态码:对业务类异常,用它指定的 HTTP code;对非业务类异常,统一500;
  2. Response Body 的错误码:异常类名
  3. Response Body 的错误描述:
  4. 对业务类异常,用它指定的错误文本;
  5. 对非业务类异常,线上可以统一文案如“服务器端错误,请稍后再试”;
  6. 开发或测试环境中用异常的 stacktrace,服务器端提供该行为的开关。

8. 超媒体API

RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。 比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。

{"link": {
  "rel":   "collection https://www.example.com/zoos",
  "href":  "https://api.example.com/zoos",
  "title": "List of zoos",
  "type":  "application/vnd.yourformat+json"
}}

上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。 Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。

{
  "current_user_url": "https://api.github.com/user",
  "authorizations_url": "https://api.github.com/authorizations",
  // ...
}

从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。

{
  "message": "Requires authentication",
  "documentation_url": "https://developer.github.com/v3"
}

上面代码表示,服务器给出了提示信息,以及文档的网址。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!