Restful API设计规范及实战

Restful API的概念在此就不费口舌了,博友们网上查哈定义文章不少,直入正题吧:php

首先抛出一个问题:
判断id为 用户下,名称为 使命召唤14(COD14) 的产品是否存在(话说我仍是很喜欢玩相似二战的使命召唤这款额,题外话...)?若是这个问题出如今 MVC 项目中,我想咱们通常会这样设计:html

api/products/isexist/{userId}/{productName}

我想你应该发现一些问题了,这种写法彻底是 MVC 的方式,但并不适用于 WebAPI,主要有三个问题:
Route 定义混乱,彻底违背 REST API URI 的一些设计原则。Action 命名不恰当。bool 返回值不合适。对于上面的三个问题,咱们分别来探讨下。

1. URI 设计首先,咱们知道在 REST API 中,URI 表明的是一种资源,它的设计要知足两个基本要求,第一名词而非动词,第二要能清晰表达出资源的含义,python

换句话说就是,从一个 URI 中,你能够很直接明了的知道访问的资源是什么,咱们再来看咱们设计的 URI:git

api/products/isExist/{userId}/{productName}

这是神马玩意啊???这种设计彻底违背 URI 原则,首先,咱们先梳理一下,咱们想要请求的资源是什么?没错,是产品(Products),但这个产品是某一个用户下的,github

因此用户和产品有一个上下级关系,访问产品首先得访问用户,这一点要在 URI 中进行体现,其次,咱们是获取产品?仍是判断产品是否存在?这个概念是不一样的,面试

产品的惟一标识和用户同样,都是 id,在 URI 的通常设计中,若是要访问某一惟一标识下的资源(好比 id 为 1 的 product),会这样进行设计:json

api/products/{id}

HttpClient 请求中会用 HttpGet 方法(api/products/1),这样咱们就能够得到一个 id 为 1 的 product,但如今的场景是,获取产品不经过惟一标识,而是经过产品名称,难道咱们要这样设计:api

api/products/{productName}

咋看之下,这样好像设计也没毛病啊,但总以为有些不对劲,好比若是再加一个产品大小,难道要改为这样:api/products/{productName}/{productSize},这种设计彻底是不恰当的,上面说到,ruby

URI 表明的是一种资源,经过 URI 获取资源的惟一方式是经过资源的惟一标识,除此以外的获取均可以看做是对资源的查询(Query),因此,针对咱们的应用场景,URI 的设计应该是这样(正确):服务器

格式标准: api/users/{userId}/products:
示 例  : api/users/1/products?productName=使命召唤COD14

上面的 URI 清晰明了的含义:查询 id 为 1 用户下名称为 COD14 的产品。

2. Action 命名对于 IsExist 的命名,若是没有很强的强迫症,其实也是能够接受的,由于 WebAPI 的 URI 并不会像 MVC 的 Route 设计那样,在访问的时候,URL 通常会默认 Action 的名字,因此,

在 WebAPI Action 设计的时候,会在 Action 前面加一个 Route 属性,用来配置 URI,也就是说每个 Action 操做会对应一个 URI 请求操做,这个请求操做也就是 HTTP 的经常使用方法。

若是咱们想把 IsExist 改掉,那用什么命名会好些呢?Action 的命名和 HTTP 方法同样,好比 Get 就是 Get,而不是 GetById,Get 是动词,表示它对资源的一种操做,具体是经过什么进行操做?

在参数中能够很直观的进行反应,通常会在 HelpPage 中进行注释说明。

IsExist 的含义仍是判断资源是否存在,其本质上来讲就是去获取一个资源,也就是 Get 操做,因此,在 WebAPI Action 中对这样的命名,咱们直接使用 Get 会好一下,或者使用 Exist。

3. 请求返回bool 通常是用在项目方法中的返回值,若是用在 HTTP 请求中,就不是很恰当了。

上面只是介绍了简单的设计场景,回归正题,继续:

设计方法及原则:

1. 使用HTTP方法:

HTTP1.1的规范定义了8个动词,然而HTTP做为一个规范并无被严格地遵照着,在大多数状况下POST是能够完成除任何种类的请求,因此如今不少的API设计都是只是用GET和POST来调用API,

在这种状况下,通常的作法是使用GET用来获取资源,其余的行为都是用POST来完成,而为了区别不一样的行为,每每在API的Uri中加入动词,如百度推送的以下API:

[ POST ] /rest/3.0/app/del_tag

功能

删除一个已存在的tag

参数

参数名 类型 必需 限制 描述
tag string 1~128字节,但不能为‘default’ 标签名称

返回值

名称 类型 描述
tag string 标签名称
result number 状态 0:建立成功; 1:建立

更清晰API设计的可能会使用GET POST PUT DELETE四种方法分别表明“查询、添加、更新、删除”等四个动做,这在概念上是符合HTTP规范的,如Google的以下API:
DELETE https://www.googleapis.com/bigquery/v2/projects/datasets/?key={YOUR_API_KEY}

在我看来,没有绝对的好与很差。若是使用第一种方法,那么只要保证Uri的语义清晰,其实和使用第二种方法没有太大的区别。

2. Uri格式:

Uri在REST中标识了一个资源,可是在具体的API设计中,每每不能作到彻底的对于资源的映射,本文中的设计将参考比较流行的Uri设计,大体有这么几条:

  • Uri的根(root, /)应当可以标识这是一个RESTful API,以与同目录下其余可能存在的资源进行区分。
  • 紧接着Uri的根,应当标识当前API的版本号。
  • 若是方法是POST或者PUT,尽可能避免使用URL编码的参数,尽可能保持Uri的干净。
  • 若是方法是DELETE,Uri应当彻底标识了须要删除的对象或者对象的集合,避免在DELETE的请求中使用其余参数,由于某些服务器可能会丢弃伴随着DELETE发送的内容。

这里仍是拿行业标杆Google的开放API来举例:

3. 固定返回码

REST的大部分实现都是一个基于HTTP的,那么天然而然就少不了与返回码打交道,然而不幸的是,HTTP的返回码定义的看起来十分随意,不少错误信息语意不详,并且在实际的开发中,

API的使用者须要处理链路的问题(如超时等)、种类繁多的HTTP返回码、和实际的返回内容,不堪其繁琐。更严重的是,这些返回码大多最终依赖于服务端开发者的具体实现,

而这种看似约定的东西分别在客户端和服务端开发者眼中的含义可能相去甚远。

那么从需求入手,咱们在使用RESTful API时须要使用返回码的缘由大体是这样的:客户端在调用一个API以后,须要在接收到的反馈必需要可以标识此次调用是否成功,

若是不成功,客户端须要拿到失败的缘由。咱们能够在API设计时做一个小小的约定,就能完美的知足以上需求了。

服务端在成功接收到客户端的请求以后,永远返回200,具体成功与否及进一步的信息放入返回的内容。

在这个场景中,若是是链路出了问题或者服务器错误等(返回码不等于200),客户端很容易就能捕获这个错误,若是链路没问题,那么出错与否在获取到的反馈内容中会有详细的描述。

4. 固定返回结构

如今愈来愈多的API设计会使用JSON来传递数据,本文中的设计也将使用JSON。JSON-RPC是一个基于JSON的广为人知的设计简洁的RPC规范,这里将借鉴JSON-RPC的响应对象的设计。

JSON-RPC中服务端响应对象的设计的基本理念是,只要调用成功,服务端必须响应数据,而响应数据的格式在任何状况下都应当是一致的,JSON-RPC的响应格式是这么设计的:

{"jsonrpc": "2.0", "result": 19, "id": 1}

{
    "jsonrpc": "2.0", 
    "error": 
        {
            "code": -23400, 
            "message": "Invalid Request"
        }, 
    "id": null
}

因为JSON-RPC的目标是创建一个通用的规范,因此响应格式的设计仍是有些复杂,咱们能够只取其中它对于error对象的设计,全部返回的格式必须是这样的:

{
    "code": -23400, 
    "message": "Invalid Request”, 
    “data”:{ }
}

这种格式的设计在许多大公司的开放API中也较为常见,好比做为行业标杆的Google,在调用Google开放平台的某API后获取到的错误数据以下,其设计思想与这里讨论的这种返回格式的思想一模一样。

{"error": {
    "errors": [
            {
                "domain": "global",
                "reason": "required",
                "message": "Login Required",
                "locationType": "header",
                "location": "Authorization"
            }
        ],
    "code": 401,
    "message": "Login Required"
    }
}
综上所述,咱们这里所探讨的API设计应该是这样的:
  1. 全部API的Uri为基于HTTP的名词性短语,用来表明一种资源。

  2. Uri格式如文中所述。

  3. 使用GET POST PUT DELETE四种方法分别表明对资源的“查询、添加、更新、删除”。

  4. 服务端接收到客户端的请求以后,统一返回200,若是客户端获取到的返回码不是200,表明链路上某一个环节出了问题。

  5. 服务端全部的响应格式为:

     {   
         “code”: -23400, 
         “message”: “Invalid Request”, 
         “data”:{ }
     }

    他们的含义分别表明:

    • code为0表明调用成功,其余会自定义的错误码;
    • message表示在API调用失败的状况下详细的错误信息,这个信息能够由客户端直接呈现给用户,不然为空;
    • data表示服务端返回的数据,具体格式由服务端自定义,API调用错误为空

还没完。。。。。这可能写的又臭又长...可是下面是回归重点额,不管在面试仍是处于本身开发项目中,restful api的设计规范仍是颇有必要知晓滴。继续个人废话:

使用的名词而不是动词

不该该使用动词:

/getAllResources 
/createNewResources 
/deleteAllResources

GET方法和查询参数不能改变资源状态:

若是要改变资源的状态,使用PUT、POST、DELETE。下面是错误的用GET方法来修改user的状态:

GET /users/211?activate
GET /users/211/activate

Rest的核心原则是将你的API拆分为逻辑上的资源。这些资源经过HTTP被操做(GET,POST,PUT,DELETE,关于Http的几种状态,请参考我以前写的一篇:https://www.cnblogs.com/phpper/p/9127553.html)

咱们定义资源ticket、user、group:

GET /tickets # 获取ticket列表

GET /tickets/12 # 查看某个具体的ticket

POST /tickets # 新建一个ticket

PUT /tickets/12 #新建ticket 12

DELETE /tickets/12 # 删除ticket 12

只须要一个endpoint:/tickets,再也没有其余什么命名规则和url规则了。

一个能够遵循的规则是:虽然看起来使用复数来描述某一个资源看起来特别扭,可是统一全部的endpoint,使用复数使得你的URL更加规整。这让API使用者更加容易理解,对开发者来讲也更容易实现。

处理关联:

GET /tickets/12/messages # 获取ticket 12的message列表

GET /tickets/12/messages/5 #获取ticket 12的message 5

POST /tickets/12/messages 建立ticket 12的一个message

PUT /tickets/12/messages/5 更新ticket 12的message 5

DELETE /tickets/12/messages/5 删除ticket 12的message 5

避免层级过深的URI

/ 在url中表达层级,用于按实体关联关系进行对象导航,通常根据id导航。

过深的导航容易致使url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4,尽可能使用查询参数代替路劲中的实体导航,如GET /animals?zoo=1&area=3

结果过滤,排序,搜索

url最好越简短越好,对结果过滤、排序、搜索相关的功能都应该经过参数实现。

过滤:例如你想限制GET /tickets 的返回结果:只返回那些open状态的ticket, GET /tickets?state=open 这里的state就是过滤参数。

排序:和过滤同样,一个好的排序参数应该可以描述排序规则,而不和业务相关。复杂的排序规则应该经过组合实现。排序参数经过 , 分隔,排序参数前加 - 表示降序排列。

  • GET /tickets?sort=-priority #获取按优先级降序排列的ticket列表

  • GET /tickets?sort=-priority,created_at #获取按优先级降序排列的ticket列表,在同一个优先级内,先建立的ticket排列在前面。

搜索:有些时候简单的排序是不够的。咱们可使用搜索技术来实现

  • GET /tickets?q=return&state=open&sort=-priority,create_at # 获取优先级最高且打开状态的ticket,并且包含单词return的ticket列表。

限制API返回值的域

有时候API使用者不须要全部的结果,在进行横向限制的同时(例如值返回API结果的前十个),还应该能够进行纵向限制,而且这个功能能有效的提升网络带宽使用率和速度。可使用fields查询参数来限制返回的域例如:

  • GET /tickets?fields=id,subject,customer_name,updated_at&state=open&sort=-updated_at

Response不要包装

response 的 body直接就是数据,不要作多余的包装。错误实例:

{
    "success":true,
    "data":{"id":1, "name":"周伯通"}
}

更新和建立操做应该返回资源

在POST操做之后,返回201created 状态码,而且包含一个指向新资源的url做为返回头。

命名方式

是蛇形命名仍是驼峰命名?若是使用json那么最好的应该是遵照JavaScript的命名方法-驼峰命名法。Java、C# 使用驼峰,python、ruby使用蛇形。

默认使用pretty print格式,开启gzip

开启pretty print返回结果会更加友好易读,并且额外的传输也能够忽略不计。若是忘了使用gzip那么传输效率将会大大减小,损失大大增长。

GitHub v3S实践经验

1.Current Version(当前版本)

经过Accept字段来区分app版本号,而不是在url中嵌入版本号(好比迭代的v1,v2,v3等): 

Accept: application/vnd.github.v3+json

2.Schema(计划)

Summary Representation

当你请求获取某一资源的列表时,响应仅返回资源的属性子集。有些属性对API来讲代价是很是高的,出于性能的考虑,会排除这些属性。要获取这些属性,请求"detailed" representation。

Example:当你获取仓库的列表时,你得到的是每一个仓库的summary representation。

GET /orgs/octokit/repos

Detailed Representation(详细描述)

当你获取一个单独的资源时,响应会返回这个资源的全部属性。

Example:当你获取一个单独的仓库,你会得到这个仓库的detailed representation。

GET /repos/octokit/octokit.rb

3.Parameters(参数)

许多API都带有可选参数。对于GET请求,任何不做为路径构成部分的参数均可以经过HTTP查询参数传入。

GET https://api.github.com/repos/vmg/redcarpet/issues?state=closed

在这个例子中,'vmg' 和 'redcarpet' 做为 :owner 和 :repo 的参数,而 :state 做为查询参数。

对于POST、PATCH、PUT和DELETE的请求,不包含在URL中的参数须要编码成JSON传递,且 Content-Type为 'application/json'。

Root Endpoint(根节点)

你能够对根节点GET请求,获取根节点下的全部API分类。

Client Errors(客户端错误)

有三种可能的客户端错误,在接收到请求体时:

发送非法JSON会返回 400 Bad Request.

HTTP/1.1 400 Bad Request
Content-Length: 35

{"message":"Problems parsing JSON"}

发送错误类型的JSON值会返回 400 Bad Request.

HTTP/1.1 400 Bad Request
Content-Length: 40

{"message":"Body should be a JSON object"}

发送无效的值会返回 422 Unprocessable Entity.

HTTP/1.1 422 Unprocessable Entity
Content-Length: 149

{
      "message": "Validation Failed",
      "errors": [
    {
      "resource": "Issue",
      "field": "title",
      "code": "missing_field"
    }
  ]
}

咱们能够告诉发生了什么错误,下面是一些可能的验证错误码:

Error Name Description
missing 资源不存在
missing_field 资源必需的域没有被设置
invalid 域的格式非法
already_exists 另外一个资源的域的值和此处的相同,这会发生在资源有惟一的键的时候

HTTP Redirects(HTTP重定向)

API v3在合适的地方使用HTTP重定向。客户端应该假设任何请求都会致使重定向。重定向在响应头中有一个 Location 的域,此域包含了资源的真实位置。

HTTP Verbs(HTTP动词)

API v3力争使用正确的HTTP动词来表示每次请求。

Verb Description
HEAD 对任何资源仅请求头信息
GET 获取资源
POST 建立资源
PATCH 使用部分的JSON数据更新资源
PUT 取代资源或资源集合
DELETE 删除资源

Hypermedia(超媒体)

不少资源有一个或者更多的 *_url 属性指向其余资源。这意味着服务端提供明确的URL,这样客户端就没必要要本身构造URL了。

Pagination(分页)

请求资源列表时会进行分页,默认每页30个。当你请求后续页的时候可使用 ?page 参数。对于某些资源,你能够经过参数 ?per_page自定义每页的大小。

curl 'https://api.github.com/user/repos?page=2&per_page=100'

须要注意的一点是,页码是从1开始的,当省略参数 ?page 时,会返回首页。

Basics of Pagination(分页基础)

关于分页的其余相关信息在响应的头信息的 Link 里提供。好比,去请求一个搜索的API,查找Mozilla的项目中哪些包含词汇addClass :

curl -I "https://api.github.com/search/code?q=addClass+user:mozilla"

头信息中Link字段以下:

Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2>; rel="next", <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last"

rel="next" 表示下一页是 page=2。也就是说,默认状况下全部的分页请求都是从首页开始。rel="last" 提供更多信息,表示最后一页是34。即咱们还有33页的信息包含addClass。

总之,咱们应该依赖于Link提供的信息,而不要尝试本身去猜或者构造URL。

Navigating through the pages

既然已经知道会接收多少页面,咱们能够经过页面导航来消费结果。咱们能够经过传递一个page参数,例如跳到14页:

curl -I "https://api.github.com/search/code?q=addClass+user:mozilla&page=14"

这是头信息中Link字段:

Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15>; rel="next",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="first",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=13>; rel="prev"

咱们会得到更多的信息,rel="first"表示首页,rel="prev"表示前一页的页码。经过这些信息,咱们能够构造一个UI界面让用户在first、previous、next、last之间进行跳转。

Rate Limiting(速率限制)

对于认证的请求,能够每小时最多请求5000次。对于没有认证的请求,限制在每小时60次请求。

检查返回的HTTP头,能够看到当前的速率限制:

curl -i https://api.github.com/users/whatever   
                                                   
HTTP/1.1 200 OK
Server: GitHub.com
Date: Thu, 27 Oct 2016 03:05:42 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 1219
Status: 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 48
X-RateLimit-Reset: 1477540017

header头信息告诉你当前的速率限制状态:

Header Name Description
X-RateLimit-Limit 当前用户被容许的每小时请求数
X-RateLimit-Remaining 在当前发送窗口内还能够发送的请求数
X-RateLimit-Reset 按当前速率发送后,发送窗口重置的时间

一旦你超过了发送速率限制,你会收到一个错误响应:

HTTP/1.1 403 Forbidden
Date: Tue, 20 Aug 2013 14:50:41 GMT
Status: 403 Forbidden
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1377013266

{
       "message": "API rate limit exceeded for xxx.xxx.xxx.xxx. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
       "documentation_url": "https://developer.github.com/v3/#rate-limiting"
}

User Agent Required

全部的API请求必须包含一个有效的 User-Agent 头。请求头不包含User-Agent的请求会被拒绝。

Conditional requests

大多数响应都会返回一个 ETag 头。不少响应也会返回一个 Last-Modified 头。你可使用这些头信息对这些资源进行后续请求,分别使用 If-None-Match 和 If-Modified-Since头。若是资源没有发生改变,服务器端会返回 304 Not Modified

Enchant REST API 实践经验

Requests

Limited HTTP Clients

若是你使用的HTTP客户端不支持PUT、PATCH、DELETE方法,发送一个POST请求,头信息里包含X-HTTP-Method-Override字段,它的值是实际须要的动词。

$ curl -u email:password https://site.enchant.com/api/v1/users/543abc \
    -X POST \
    -H "X-HTTP-Method-Override: DELETE"

Rate Limiting

全部响应的头部包含描述当前限流状态的字段:

Rate-Limit-Limit: 100
Rate-Limit-Remaining: 99
Rate-Limit-Used: 1
Rate-Limit-Reset: 20
  • Rate-Limit-Limit - 当前时间段内容许的总的请求数

  • Rate-Limit-Remaining - 当前时间段内还剩余的请求数

  • Rate-Limit-Used - 本次所使用的请求数

  • Rate-Limit-Reset - 重置所需秒数

若是速率限制被打破,API会返回 429 Too Many Requests 的状态码。在这种状况下,你的应用不该该再发送任何请求直到 Rate-Limit-Reset 所规定的时间过去。

Field Filtering(字段过滤)

你能够本身限制响应返回的域。只须要你传递一个 fields 参数,用逗号分隔所须要的域,好比:

GET /api/v1/users?fields=id,first_name

Counting

全部返回一个集合的URL,都会提供count统计全部结果的个数。要获取count值须要加一个 count=true 的参数。count会在消息头中的Total-Count 字段中返回。

GET /api/v1/tickets?count=true

200 OK
Total-Count: 135
Rate-Limit-Limit: 100
Rate-Limit-Remaining: 98
Rate-Limit-Used: 2
Rate-Limit-Reset: 20
Content-Type: application/json

count表示全部现存结果的数量,而不是这次响应返回的结果的数量。

Enveloping

若是你的HTTP客户端难以读取状态码和头信息,咱们能够将全部都打包进响应消息体中。咱们只须要传递参数 envelope=true,而API会始终返回200的HTTP状态码。真正的状态码、头信息和响应都在消息体中。

GET /api/v1/users/does-not-exist?envelope=true
200 OK
{
      "status": 404,
      "headers": {
    "Rate-Limit-Limit": 100,
    "Rate-Limit-Remaining": 50,
    "Rate-Limit-Used": 0,
    "Rate-Limit-Reset": 25
  },
  "response": {
    "message": "Not Found"
  }
}

其余如 分页、排序等,enchant的设计规范和GitHub v3大体相同。有兴趣的朋友能够了解下相关的资料。

另外发现一款提高开发效率的接口管理工具,体验很好,涵盖文档管理、团队协做以及接口测试,eoLinker接口管理平台:https://www.eolinker.com,感兴趣的朋友能够体验哈。

相关文章
相关标签/搜索