首先抛出一个问题:
判断id为 用户下,名称为 使命召唤14(COD14) 的产品是否存在(话说我还是很喜欢玩类似二战的使命召唤这款额,题外话...)?如果这个问题出现在 MVC 项目中,我想我们一般会这样设计:
api/products/isexist/{userId}/{productName}
我想你应该发现一些问题了,这种写法完全是 MVC 的方式,但并不适用于 WebAPI,主要有三个问题:
Route 定义混乱,完全违背 REST API URI 的一些设计原则。Action 命名不恰当。bool 返回值不合适。对于上面的三个问题,我们分别来探讨下。
1. URI 设计首先,我们知道在 REST API 中,URI 代表的是一种资源,它的设计要满足两个基本要求,第一名词而非动词,第二要能清晰表达出资源的含义,
换句话说就是,从一个 URI 中,你可以很直接明了的知道访问的资源是什么,我们再来看我们设计的 URI:
api/products/isExist/{userId}/{productName}
这是神马玩意啊???这种设计完全违背 URI 原则,首先,我们先梳理一下,我们想要请求的资源是什么?没错,是产品(Products),但这个产品是某一个用户下的,
所以用户和产品有一个上下级关系,访问产品首先得访问用户,这一点要在 URI 中进行体现,其次,我们是获取产品?还是判断产品是否存在?这个概念是不同的,
产品的唯一标识和用户一样,都是 id,在 URI 的一般设计中,如果要访问某一唯一标识下的资源(比如 id 为 1 的 product),会这样进行设计:
api/products/{id}
HttpClient 请求中会用 HttpGet 方法(api/products/1),这样我们就可以获得一个 id 为 1 的 product,但现在的场景是,获取产品不通过唯一标识,而是通过产品名称,难道我们要这样设计:
api/products/{productName}
咋看之下,这样好像设计也没毛病啊,但总觉得有些不对劲,比如如果再加一个产品大小,难道要改成这样:api/products/{productName}/{productSize},这种设计完全是不恰当的,上面说到,
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来举例:
POST
https://www.googleapis.com/books/v1/mylibrary/annotations
PUT
https://www.googleapis.com/bigquery/v2/projects/p1/datasets/p2
DELETE
https://www.googleapis.com/bigquery/v2/projects/{project-parameter}/datasets/{datasets-parameter}
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设计应该是这样的:
-
所有API的Uri为基于HTTP的名词性短语,用来代表一种资源。
-
Uri格式如文中所述。
-
使用GET POST PUT DELETE四种方法分别代表对资源的“查询、添加、更新、删除”。
-
服务端接收到客户端的请求之后,统一返回200,如果客户端获取到的返回码不是200,代表链路上某一个环节出了问题。
-
服务端所有的响应格式为:
{ “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使用者更加容易理解,对开发者来说也更容易实现。
处理关联:
1 2 3 4 5 6 7 8 9 |
|
避免层级过深的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(客户端错误)
有三种可能的客户端错误,在接收到请求体时:
1 发送非法JSON会返回 400 Bad Request.
HTTP/1.1 400 Bad Request Content-Length: 35 {"message":"Problems parsing JSON"}
2 发送错误类型的JSON值会返回 400 Bad Request.
HTTP/1.1 400 Bad Request Content-Length: 40 {"message":"Body should be a JSON object"}
3 发送无效的值会返回 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大致相同。有兴趣的朋友可以了解下相关的资料。