Restful API 设计思路及实践

3,640 阅读8分钟
原文链接: www.jianshu.com

记得第一次写APP的时候,那时还完全不知道REST这个东西,对Web Service也是一知半解。我和另一个同学在讨论使用什么协议来交互时,通过各自充分的调研之后(其实就是搜索引擎找一找。。。),一致认为,HTTP这个东西本身就对带宽的消耗这么大了,这么多Web Service(当时还是SOAP当道)还是基于HTTP之上的,这得浪费多少带宽啊。最后一致决定使用Socket来通信,现在想想当时也是挺不容易的,我们硬是在Socket上搭了一套通信协议,还发展到了第二版。

今天在移动应用普及、前后端分离的大浪潮下,RESTful风格的API大行其道,可是因为它本身就是一个比较模糊且宽泛的概念,所以每个人对它的理解都有千差万别。我觉得我们在技术选型的时候,在自己的技术积累以及参考已有的行业最佳实践的基础上,应当首先考虑自身系统的需求,思考「选择某一种技术」会对系统的开发和维护带来哪些好处与坏处,而不是人云亦云看着别人用什么自己就用什么。而且RESTful API设计目前并没有一个公认的行业最佳实践,故而开发者在设计一个API系统时,更应该根据自身的情况量身定制,千万不要说「我照着某某公司的开放API照搬」就好了。 本文将根据我使用REST的经验来总结一下RESTful API设计的一些知识和经验,自勉。本文将不讨论Oauth等安全问题。

首先理清一些概念:
  • REST(Representational State Transfer)
    定义了一套基于Web的数据交互方式的设计风格。
  • RESTful
    符合REST风格的API就可以叫做RESTful API。注意,本文讲到的RESTful API设计方法将是基于HTTP和JSON实现方式,但不论HTTP还是JSON都不是REST的标准。REST只是风格,没有标准。
  • 动词、RPC
    在微信里搜索【RESTful API 设计】,出来好多文章都是说怎么在RESTful Uri里使用动词等等,这些人除了一部分是把文章拿来抄一抄的,其他的其实搞混了REST和RPC的概念了,REST强调资源,RPC强调动作,所以REST的Uri组成为名词,RPC多为动词短语。然而它们也并不是完全不相关的两个东西,本文中的实现就会参考一部分JSON-RPC的设计思想。
  • Web Service
    这个是一个更古老的概念,有一套它的理论,不过我更倾向于把它理解成任何基于Web提供的服务。

设计方法及原则:

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:

Request

DELETE www.googleapis.com/bigquery/v2…

Response

404 Not Found

– Show headers –

Not Found

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

2. Uri格式:

Uri在REST中标识了一个资源,但是在具体的API设计中,往往不能做到完全的对于资源的映射,本文中的设计将参考比较流行的Uri设计,大致有这么几条:

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

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

POST www.googleapis.com/books/v1/my…

PUT www.googleapis.com/bigquery/v2…

DELETE www.googleapis.com/bigquery/v2…

3. 固定返回码

REST的大部分实现都是一个基于HTTP的,那么自然而然就少不了与返回码打交道,然而不幸的是,HTTP的返回码定义的看起来十分随意,很多错误信息语焉不详,而且在实际的开发中,API的使用者需要处理链路的问题(如超时等)、种类繁多的HTTP返回码、和实际的返回内容,不堪其繁琐。更严重的是,这些返回码大多最终依赖于服务端开发者的具体实现,而这种看似约定的东西分别在客户端和服务端开发者眼中的含义可能相去甚远。

那么从需求入手,我们在使用RESTful API时需要使用返回码的原因大致是这样的:客户端在调用一个API之后,需要在接收到的反馈必须要能够标识这次调用是否成功,如果不成功,客户端需要拿到失败的原因。我们可以在API设计时作一个小小的约定,就能完美的满足以上需求了。

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

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

4. 固定返回结构

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

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

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

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

A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".

result

This member is REQUIRED on success.

This member MUST NOT exist if there was an error invoking the method.

The value of this member is determined by the method invoked on the Server.

error

This member is REQUIRED on error.
This member MUST NOT exist if there was no error triggered during invocation.

The value for this member MUST be an Object as defined in section 5.1.

id

This member is REQUIRED.

It MUST be the same as the value of the id member in the Request Object.

If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it MUST be Null.

由于JSON-RPC的目标是建立一个通用的规范,所以响应格式的设计还是有些复杂,我们可以只取其中它对于error对象的设计,所有返回的格式必须是这样的:

{
    "code": -32600, 
    "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”: -32600, 
         “message”: “Invalid Request”, 
         “data”:{ }
     }

    他们的含义分别代表:

    • code为0代表调用成功,其他会自定义的错误码;
    • message表示在API调用失败的情况下详细的错误信息,这个信息可以由客户端直接呈现给用户,否则为空;
    • data表示服务端返回的数据,具体格式由服务端自定义,API调用错误为空

我的博客