一个好的API设计应该可以减少很多不必要的沟通成本和解决开发人员之间的一些设计理念的歧义,即看到接口文档基本上就不会再和你BB了
定义
先了解下几个重要的术语:
-
资源:一个对象的单独实例,如一只动物
-
集合:一群同种对象,如动物
-
幂等:无边际效应,多次操作得到相同的结果
动词
显然你了解GET和POST请求。当你用浏览器去访问不同页面的时候,这两个是最常见的请求。POST术语如此流行以至于开始侵扰通俗用语。即使是那些不知道互联网如何工作的人们也能“post”一些东西到朋友的Facebook墙上。
这里至少有四个非常重要的HTTP动词需要你知道。
-
GET (选择):从服务器上获取一个具体的资源或者一个资源列表。
-
POST (创建): 在服务器上创建一个新的资源。
-
PUT (更新):以整体的方式更新服务器上的一个资源。
-
DELETE (删除):删除服务器上的一个资源。
其他的由于考虑到兼容性问题,暂时不用:
-
HEAD : 获取一个资源的元数据,如数据的哈希值或最后的更新时间。
-
OPTIONS:获取客户端能对资源做什么操作的信息。
-
PATCH (更新):只更新服务器上一个资源的一个属性。
一个好的RESTful API只允许第三方调用者使用这四个HTTP动词进行数据交互,并且在URL段里面不出现任何其他的动词。
API版本
无论你正在构建什么,无论你在入手前做了多少计划,你核心的应用总会发生变化,数据关系也会变化,资源上的属性也会被增加或删除。只要你的项目还活着,并且有大量的用户在用,这种情况总是会发生。
请谨记一点,API是服务器与客户端之间的一个公共契约。如果你对服务器上的API做了一个更改,并且这些更改无法向后兼容,那么你就打破了这个契约,客户端又会要求你重新支持它。为了避免这样的事情,你既要确保应用程序逐步的演变,又要让客户端满意。那么你必须在引入新版本API的同时保持旧版本API仍然可用。
随着时间的推移,你可能声明不再支持某些旧版本的API。申明不支持一个特性并不意味着关闭或者破坏它。而是告诉客户端旧版本的API将在某个特定的时间被删除,并且建议他们使用新版本的API。
一个好的RESTful API会在URL中包含版本信息。另一种比较常见的方案是在请求头里面保持版本信息。但是在请求头里面包含版本信息远没有放在URL里面来的容易。
API根URL
无论你信不信,API的根地址很重要。当一个开发者接手了一个旧项目(如进行代码考古时)。而这个项目正在使用你的API,同时开发者还想构建一个新的特性,但他们完全不知道你的服务。幸运的是他们知道客户端对外调用的那些URL列表。让你的API根入口点保持尽可能的简单是很重要的,因为开发者很可能一看到那些冗长而又复杂的URL就转身而走。
这里有两个常见的URL根例子:
-
https://example.org/api/v1/*
-
https://api.example.com/v1/*
如果你的应用很庞大或者你预期它将会变的很庞大,那么将API放到子域下通常是一个好选择。这种做法可以保持某些规模化上的灵活性。
端点
一个端点就是指向特定资源或资源集合的URL。
如果你正在构建一个虚构的API来展现几个不同的动物园,每一个动物园又包含很多动物,员工和每个动物的物种,你可能会有如下的端点信息:
-
https://api.example.com/v1/zoos
-
https://api.example.com/v1/animals
-
https://api.example.com/v1/animal_types
-
https://api.example.com/v1/employees
针对每一个端点来说,你可能想列出所有可行的HTTP动词和端点的组合。如下所示,请注意我把HTTP动词都放在了虚构的API之前,正如将同样的注解放在每一个HTTP请求头里一样。
-
GET /zoos: List all Zoos (ID and Name, not too much detail)
-
POST /zoos: Create a new Zoo
-
GET /zoos/ZID: Retrieve an entire Zoo object
-
PUT /zoos/ZID: Update a Zoo (entire object)
-
DELETE /zoos/ZID: Delete a Zoo
-
GET /zoos/ZID/animals: Retrieve a listing of Animals (ID and Name).
-
GET /animals: List all Animals (ID and Name).
-
POST /animals: Create a new Animal
-
GET /animals/AID: Retrieve an Animal object
-
PUT /animals/AID: Update an Animal (entire object)
-
GET /animal_types: Retrieve a listing (ID and Name) of all Animal Types
-
GET /animal_types/ATID: Retrieve an entire Animal Type object
-
GET /employees: Retrieve an entire list of Employees
-
GET /employees/EID: Retreive a specific Employee
-
GET /zoos/ZID/employees: Retrieve a listing of Employees (ID and Name) who work at this Zoo
-
POST /employees: Create a new Employee
-
POST /zoos/ZID/employees: Hire an Employee at a specific Zoo
-
DELETE /zoos/ZID/employees/EID: Fire an Employee from a specific Zoo
在上面的列表里,ZID表示动物园的ID, AID表示动物的ID,EID表示雇员的ID,还有ATID表示物种的ID。让文档里所有的东西都有一个关键字是一个好主意。
为了简洁起见,我已经省略了所有API共有的URL前缀。作为沟通方式这没什么问题,但是如果你真要写到API文档中,那就必须包含完整的路径(如,GET http://api.example.com/v1/animal_type/ATID)。
请注意如何展示数据之间的关系,特别是雇员与动物园之间的多对多关系。通过添加一个额外的URL段就可以实现更多的交互能力。当然没有一个HTTP动词能表示正在解雇一个人,但是你可以使用DELETE一个动物园里的雇员来达到相同的效果。
过滤器
当客户端创建了一个请求来获取一个对象列表时,很重要一点就是你要返回给他们一个符合查询条件的所有对象的列表。这个列表可能会很大。但你不能随意给返回数据的数量做限制。因为这些无谓的限制会导致第三方开发者不知道发生了什么。如果他们请求一个确切的集合并且要遍历结果,然而他们发现只拿到了100条数据。接下来他们就不得不去查找这个限制条件的出处。到底是服务端的bug导致的,还是因为网络截断了大数据包?
尽可能减少那些会影响到第三方开发者的无谓限制。
这点很重要,但你可以让客户端自己对结果做一些具体的过滤或限制。这么做最重要的一个原因是可以最小化网络传输,并让客户端尽可能快的得到查询结果。其次是客户端可能比较懒,如果这时服务器能对结果做一些过滤或分页,对大家都是好事。另外一个不那么重要的原因是(从客户端角度来说),对服务器来说响应请求的负载越少越好。
过滤器是最有效的方式去处理那些获取资源集合的请求。所以只要出现GET的请求,就应该通过URL来过滤信息。以下有一些过滤器的例子,可能是你想要填加到API中的:
-
?limit=10: 减少返回给客户端的结果数量(用于分页)
-
?offset=10: 发送一堆信息给客户端(用于分页)
-
?animaltypeid=1: 使用条件匹配来过滤记录
-
?sortby=name&order=asc: 对结果按特定属性进行排序
不管怎样,服务端对接口数据的限制条件都必须对客户端可见。
状态码
对于一个RESTful API来说很重要的一点就是要使用HTTP的状态码,因为它们是HTTP的标准。很多的网络设备都可以识别这些状态码,例如负载均衡器可能会通过配置来避免发送请求到一台web服务器,如果这台服务器已经发送了很多的50x错误回来。这里有大量的HTTP状态码可以选择,但是下面的列表只给出了一些重要的代码作为一个参考:
-
200 OK – [GET] 客户端向服务器请求数据,服务器成功找到它们
-
201 CREATED – [POST/PUT/PATCH] 客户端向服务器提供数据,服务器根据要求创建了一个资源
-
204 NO CONTENT – [DELETE] 客户端要求服务器删除一个资源,服务器删除成功
-
400 INVALID REQUEST – [POST/PUT/PATCH] 客户端向服务器提供了不正确的数据,服务器什么也没做
-
404 NOT FOUND – [*] 客户端引用了一个不存在的资源或集合,服务器什么也没做
-
500 INTERNAL SERVER ERROR – [*] 服务器发生内部错误,客户端无法得知结果,即便请求已经处理成功
状态码范围
1xx范围的状态码是保留给底层HTTP功能使用的,并且估计在你的职业生涯里面也用不着手动发送这样一个状态码出来。
2xx范围的状态码是保留给成功消息使用的,你尽可能的确保服务器总发送这些状态码给用户。
3xx范围的状态码是保留给重定向用的。大多数的API不会太常使用这类状态码,但是在新的超媒体样式的API中会使用更多一些。
4xx范围的状态码是保留给客户端错误用的。例如,客户端提供了一些错误的数据或请求了不存在的内容。这些请求应该是幂等的,不会改变任何服务器的状态。
5xx范围的状态码是保留给服务器端错误用的。这些错误常常是从底层的函数抛出来的,并且开发人员也通常没法处理。发送这类状态码的目的是确保客户端能得到一些响应。收到5xx响应后,客户端没办法知道服务器端的状态,所以这类状态码是要尽可能的避免。
预期的返回文档
当使用不同的HTTP动词向服务器请求时,客户端需要在返回结果里面拿到一系列的信息。下面的列表是非常经典的RESTful API定义:
-
GET /collection: 返回一系列资源对象
-
GET /collection/resource: 返回单独的资源对象
-
POST /collection: 返回新创建的资源对象
-
PUT /collection/resource: 返回完整的资源对象
-
PATCH /collection/resource: 返回完整的资源对象
-
DELETE /collection/resource: 返回一个空文档
请注意当一个客户端创建一个资源时,她们常常不知道新建资源的ID(也许还有其他的属性,如创建和修改的时间戳等)。这些属性将在随后的请求中返回,并且作为刚才POST请求的一个响应结果。
响应结果:
{"status":状态码 int,"message":"错误提示","data":响应数据(泛型)}
认证
由于restful api 接口是无状态的,所以接口的安全性尤为重要。我采用的是jwt-token:
JWT 鉴权机制
注:每次请求头和响应头都会存储当前有效的token,白名单接口除外
1、JWT-Filter对登录/注册不鉴权,成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)
2、当该用户这次请求JWTToken值还在生命周期内,且该token对应cache中的k存在,则会通过重新PUT的方式k、v都为Token值,缓存中的token值生命周期时间重新计算(这时候k、v值一样)
3、当该用户这次请求JWTToken值还在生命周期内,但该token对应cache中的k不存在,返回用户信息已失效,请重新登录。
4、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
5、当该用户这次请求jwt生成的token值已经超时,且该token对应cache中的k不存在,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
内容类型
请求和响应都是用json交互,同时约定好金额、日期、枚举等数据的数据格式,如果需要格式化,可以在后端处理好,不然每个前端(android、ios、mweb等等都得去处理)
文档
老实说,即使你不能百分之百的遵循指南中的条款,你的API也不是那么糟糕。但是,如果你不为API准备文档的话,没有人会知道怎么使用它,那它真的会成为一个糟糕的API。
推荐两款:swagger 和 apizza