程序猿的纸条屋

REST接口设计规范

2017-03-21  本文已影响0人  零一间

REST接口设计规范

URI格式规范

URI的格式定义如下:

URI = scheme "://" authority "/" path [ "?" query ] [ "#" fragment ]  

URL是URI的一个子集(一种具体实现),对于REST API来说一个资源一般对应一个唯一的URI(URL)。在URI的设计中,我们会遵循一些规则,使接口看起透明易读,方便使用者调用。

"/"分隔符一般用来对资源层级的划分,例如 http://api.canvas.restapi.org/shapes/polygons/quadrilaterals/squares

对于REST API来说,"/"只是一个分隔符,并无其他含义。为了避免混淆,"/"不应该出现在URL的末尾。例如以下两个地址实际表示的都是同一个资源:

http://api.canvas.restapi.org/shapes/  
http://api.canvas.restapi.org/shapes

REST API对URI资源的定义具有唯一性,一个资源对应一个唯一的地址。为了使接口保持清晰干净,如果访问到末尾包含 "/" 的地址,服务端应该301到没有 "/"的地址上。当然这个规则也仅限于REST API接口的访问,对于传统的WEB页面服务来说,并不一定适用这个规则。

连字符"-"一般用来分割URI中出现的字符串(单词),来提高URI的可读性,例如:

http://api.example.restapi.org/blogs/mark-masse/entries/this-is-my-first-post  

使用下划线""来分割字符串(单词)可能会和链接的样式冲突重叠,而影响阅读性。但实际上,"-"和""对URL中字符串的分割语意上还是有些差异的:"-"分割的字符串(单词)一般各自都具有独立的含义,可参见上面的例子。而"_"一般用于对一个整体含义的字符串做了层级的分割,方便阅读,例如你想在URL中体现一个ip地址的信息:210_110_25_88 .

根据RFC3986定义,URI是对大小写敏感的,所以为了避免歧义,我们尽量用小写字符。但主机名(Host)和scheme(协议名称:http/ftp/...)对大小写是不敏感的。

例如 .php .json 之内的就不要出现了,对于接口来说没有任何实际的意义。如果是想对返回的数据内容格式标示的话,通过HTTP Header中的Content-Type字段更好一些。

资源的原型

文档是资源的单一表现形式,可以理解为一个对象,或者数据库中的一条记录。在请求文档时,要么返回文档对应的数据,要么会返回一个指向另外一个资源(文档)的链接。以下是几个基于文档定义的URI例子:

http://api.soccer.restapi.org/leagues/seattle 
http://api.soccer.restapi.org/leagues/seattle/teams/trebuchet 
http://api.soccer.restapi.org/leagues/seattle/teams/trebuchet/players/mike 

集合可以理解为是资源的一个容器(目录),我们可以向里面添加资源(文档)。例如:

http://api.soccer.restapi.org/leagues
http://api.soccer.restapi.org/leagues/seattle/teams 
http://api.soccer.restapi.org/leagues/seattle/teams/trebuchet/players  

仓库是客户端来管理的一个资源库,客户端可以向仓库中新增资源或者删除资源。客户端也可以批量获取到某个仓库下的所有资源。仓库中的资源对外的访问不会提供单独URI的,客户端在创建资源时候的URI除外。例如:

PUT /users/1234/favorites/alonso  

上面的例子我们可以理解为,我们向一个id是1234的用户的仓库(收藏夹)中,添加了一个名为alonso的资源。通俗点儿说:就是用户收藏了一个自己喜爱的球员阿隆索。

控制器资源模型,可以执行一个方法,支持参数输入,结果返回。 是为了除了标准操作:增删改查(CRUD)以外的一些逻辑操作。控制器(方法)一般定义子URI中末尾,并且不会有子资源(控制器)。例如我们向用户重发ID为245743的消息:

POST /alerts/245743/resend 

URI命名规范

例如一个资源URI可以这样定义:

http://api.soccer.restapi.org/leagues/{leagueId}/teams/{teamId}/players/{playerId}  

其中:leagueId,teamId,playerId 是变量(数字,字符串都类型都可以)。

CRUD是创建,读取,更新,删除这四个经典操作的简称
例如删除的操作用REST规范执行的话,应该是这个样子:

DELETE /users/1234

以下是几个错误的示例:

GET /deleteUser?id=1234  
GET /deleteUser/1234  
DELETE /deleteUser/1234  
POST /users/1234/delete 
http://api.college.restapi.org/students/morgan/send-sms 
http://api.college.restapi.org/students/morgan/send-sms?text=hello  

以上的两个URI看起来很像,但实际的含义是有差别的。第一个URI是一个发送消息的Controller类型的API,第二个URI是发送一个text的内容是hello的消息。

在REST中,query字段一般作为查询的参数补充,也可以帮助标示一个唯一的资源。但需要注意的是,作为一个提供查询功能的URI,无论是否有query条件,我们都应该保证结果的唯一性,一个URI对应的返回数据是不应该被改变的(在资源没有修改的情况下)。HTTP中的缓存也可能缓存查询结果,这个也是我们需要知道的。

GET /users //返回所有用户列表  
GET /users?role=admin //返回权限为admin的用户列表  

如果是一个简单的列表操作,可以这样设计:

GET /users?pageSize=25&pageStartIndex=50  

如果是一个复杂的列表或查询操作的话,我们可以为资源设计一个Collection,因为复杂查询可能会涉及比较多的参数,建议使用Post的方式传入,例如这样:

POST /users/search  

HTTP交互设计

一旦资源被删除,GET/HEAD方法访问被删除的资源时,要返回404
DELETE是一个比较纯粹的方法,我们不能对其做任何的重构或者定义,不可附加其它状态条件,如果我们希望"软"删除一个资源,则这种需求应该由Controller类资源来实现。

HTTP响应状态码的使用

原数据设计

HTTP Headers

假设有两个客户端client#1/#2都向一个Store资源提交PUT请求,服务端是无法清楚的判断是要insert还是要update的,所以我们要在header中加入条件标示if-Match,If-Unmodified-Since 来明确是本次调用API的意图。例如:

client#1第一次向服务端发起一个请求 PUT /objects/2113 此时2113资源还不存在,那服务端会认为本次请求是一个insert操作,完成后,会返回 201 (“Created”)

client#2再一次向服务端发起同一个请求 PUT /objects/2113 时,因2113资源已存在,服务端会返回 409 (“Conflict”)

为了能让client#2的请求成功,或者说我们要清楚的表明本次操作是一次update操作,我们必须在header中加入一些条件标示,例如 if-Match。我们需要给出资源的ETag(if-Match:Etag),来表明我们希望更新资源的版本,如果服务端版本一致,会返回200 (“OK”) 或者 204 (“No Content”)。如果服务端发现指定的版本与当前资源版本不一致,会返回 412 (“Precondition Failed”)

数据媒体类型(Media Type)

定义如下:

Content-Type: type "/" subtype *( ";" parameter )  
两个实例:
Content-type: text/html; charset=ISO-8859-4  
Content-type: text/plain; charset="us-ascii" 

type 主类型一般为:application, audio, image, message, model, multipart, text, video。REST接口的主类型一般使用application

数据媒体类型(Media Type)设计

body的媒体格式

json是一种流行且轻便友好的格式,json是一种无序的键值对的集合,其中key是需要用双引号引起来的,value如果是数字可以不用双引号,如果是非数字的格式需要使用双引号。
这是一个json格式的例子:

{
    "firstName" : "Osvaldo",
    "lastName" : "Alonso", "firstNamePronunciation" : "ahs-VAHL-doe", "number" : 6,
    "birthDate" : "1985-11-11"
}

json是允许大小写混用命名的,但要避免使用特殊符号
除了json我们也可以使用其他常用的格式,例如xml,html等
body本身只应包含资源相关的信息,不要附加其它传输状态的信息。

错误响应描述

错误信息的格式应该保持一致,例如用以下方式(json格式):

{
  "id" : Text,  //错误唯一标示id
  "description" : Text  //错误具体描述
}

如果有多个错误,可以用json数组来描述:

{
  "elements" : [
    {
      "id" : "Update Failed",
      "description" : "Failed to update /users/1234"
    }
  ]
}

错误类型需要保持统一

客户端关注的问题

接口版本管理

接口的安全

接口数据响应的结构

只获许部分字段:

GET /students/morgan?fields=(firstName, birthDate)

不希望获取某些字段:

GET /students/morgan?fields=!(address,schedule!(wednesday, friday)) 
  "firstName" : "Morgan", 
  "birthDate" : "1992-07-31",
  # Other fields...
  "links" : {
    "self" : {
      "href" : "http://api.college.restapi.org/students/morgan",
      "rel" : "http://api.relations.wrml.org/common/self" 
    },
    "favoriteClass" : {
      "href" : "http://api.college.restapi.org/classes/japn-301",    
      "rel" : "http://api.relations.wrml.org/college/favoriteClass"
    },
    # Other links... 
  }
}

如果我们传入embed=(favoriteClass)的参数,返回的数据中将用实际的内容替换links里的对应的潜入资源:

# Request
GET /students/morgan?embed=(favoriteClass)

# Response
{
  "firstName" : "Morgan",
  "birthDate" : "1992-07-31", 
  "favoriteClass" : { //需要返回的嵌入数据
    "id" : "japn-301",
    "name" : "Third-Year Japanese", 
    "links" : {
      "self" : {
        "href" : "http://api.college.restapi.org/classes/japn-301",     
        "rel" : "http://api.relations.wrml.org/common/self"
      } 
    }
}

# Other fields...
  "links" : { 
    "self" : {
      "href" : "http://api.college.restapi.org/students/morgan",
      "rel" : "http://api.relations.wrml.org/common/self" 
    },
    # 之前的嵌入式链接favoriteClass,已被替换为实体数据
    # Other links... 
  }
}

#其中嵌入式链接信息中的 rel ,一般是对 href 资源如何交互的描述,例如是通过 GET 还是 POST 方法,可以是以下的结构:
{
  "name": "morgan",
  "method": "GET", 
  ... #其它描述字段
}

JavaScript客户端

目前主流的浏览器对JavaScript的支持越来越完善,因此对于WEB应用来说,我们完全可以把客户单看成一个JavaScript客户端。

原文出处

上一篇下一篇

猜你喜欢

热点阅读