【翻译】优秀的RESTful API的设计原则
原文地址:http://codeplanet.io/principles-good-restful-api-design/
原文作者:Thomas Hunter Ii
目录
-
- 定义(Definitions)
-
- 数据的设计与抽象化(Data Design and Abstraction)
-
- HTTP动词 (Verbs)
-
- 版本(Versioning)
-
- 分析 (Analytics)
-
- API根路径 (API Root URL)
-
- 路径 (Endpoint)
-
- 信息过滤 (Filtering)
-
- 状态码 (Status Codes)
-
- 文档返回值 (Expected Return Documents)
-
- 身份认证 (Authentication)
-
- 内容形式 (Content Type)
-
- 超媒体 (Hypermedia APIs)
-
- 文档 (Documentation)
-
- 其它:HTTP包文
引言
众所周知,优秀的API设计是相当困难的!API建立起服务器和那些希望调用数据的客户之间的联系。打破了这个约定会导致开发者收到大量的报怨邮件,大量的客户因此要修改他们不能继续使用的APP。当然API文档可以有效降低这些问题,但是大多数程序员都不爱写文档。
建立起API机制是提高你的服务的重要途径之一。通过建立API,你的服务器/请求中心会变成其它请求服务的一个平台。看看那些拥有大数据的公司:Facebook,Twitter,谷歌,GitHub,亚马逊,Netfilx,无一不是因为他们开放了数据中心的API,使得他们变得很强大。事实上,这个产业生存的最重要的原因就是那些平台所提供的消费数据。
越早建立起你的API,越多的人们会使用你的服务。
在你设计API时,及时的更新文档,这些文档的设计的目的是为了确保调用你API的客户能更容易的理解,这样你可以大幅的减少收到关于困惑或是抱怨的邮件。我将会整理设计原则方面的篇章,你可以根据你的需要读其中的一段或几段。
1. 定义 (Definitions)
以下是一些我会在这篇文章中用到的名词:
- 资源(Resource): 一个对象的记录。如:一个动物。
- 集合(Collection): 很多对象的集合。如:很多动物。
- HTTP: 互联网交互的协议。
- 客户(Consumer): 通过电脑会做出HTTP请求的客户。
- 第三方开发人员(Third Party Developer): 需要获取服务器数据的第三方开发人员。
- 服务器(Server): 一个可以处理客户请求的HTTP的服务或应用。
- 路径(Endpoint): 一个服务器API的路径,代表了一个资源或一个集合。
2. 数据的设计与抽象化 (Data Design and Abstraction)
设计API比你想象的要简单。首先你需要考虑如何设计你的数据以及你的服务/请求中心将如何工作。如果你在开始写程序的时候就应该考虑API的设计,这样会使得一切变得简单起来。但是如果你想要在已经完成的程序中添加API,你可能需要提供更多的抽象化服务。
这里,我们临时将一个集合(Collection)看做是一个数据库表,资源(Resource)看作是数据表中的一条记录。当然这是一个特殊的例子,事实上,你的API必须将你的数据和业务逻辑尽可能的剥离开来。这点是非常重要的,这样可以消除第三方调用API的开发者的困惑,否则导致的结果就是他们不想使用你的API。
当然,有些重要的服务是不能通过API暴露给第三方用户的,一个简单的例子就是很多API不允许第三方使用者调用API来新增用户。
3. HTTP动词 (Verbs)
毫无疑问,你肯定知道GET和POST请求。它们是浏览器中最常用的请求类型。POST请求非常流行以致于都快成了日常用语。很多对互联网的原理一无所知的人们却可能知道他们可以通过Facebook的友好请求,‘POST‘一些信息上去。
有关于HTTP的动作,你需要掌握四个半。其中的半个是因为‘PATCH‘ 和’PUT‘这个动作很像。对于API开发者来说,其它四个HTTP动作都是有两两联系的。下面是具体的作动,以及解释(在这里我假设大部分的读者都懂得数据库方面的知识)。
- GET(select):从服务器中取出资源。(一个或是多个)
- POST(create):在服务器上新增一个资源。
- PUT(update):在服务器上修改资源(需要提供整个资源信息)。
- PATCH(update):在服务器上修改资源(只需要提供待修改的资源信息)。
- Delete(delete):从服务器上删除资源。
以下是两个比较少用到的HTTP动作:
- HEAD:从服务器上取回单个元数据的资源,例如数据的哈希值或数据的最后更新时间等。
- OPTIONS:从服务器上取回客户允许得到的资源信息。
一个好的RESTful API会使用以上“四个半”动作来供第三方使用者获得或上传数据。并且URL不会包含任何的ACTION或是其它的动作。
浏览器通常会缓存GET请求(大多数如此!),例如一个被缓存了的GET请求(需要看缓存的HEADERS)要比POST请求快一些。一个HEAD请求一般是没有回应BODY的GET请求,当然也能被缓存。
4. 版本(Versioning)
无论你建立的是怎样的服务,在开始写程序前做了大量的设计,只要你的服务器代码有变动,那么你的数据关系就可能发生变化,比如你的数据资源可能会新增或删除一些属性。这是开发必不可少的工作,尤其是你的程序已经上线并且被很多人所使用。
请记住API是服务提供方和客户之间的联系。如果你需要改变你的服务器API,这将会破坏兼容性问题。如果你要破坏原有的API,你的客户将会痛恨你。如果你持续的修改你的API,你将会失去很多客户。所以需要你确保你的应用是逐步的更新,这样才会使得调用你的API的客户满意。你需要建立API的版本并且在有需要的时候介绍新的版本给客户,并且保持老的版本可持续访问。
另外一个建议是如果你的API只是简单的新增一些特性,例如在你的资源中新增一些属性(这些属性不是必填项,资源没有这些属性也能照常运行),或者你新增一些新的路径(Endpoints),这个时候你不必更新你的API版本号,因为你所做的这些改变并不会打破现有的兼容性。当然你需要更新你的API文档。
过段时间后,你可以不建议使用你的API的旧版本。不过不建议使用并不意味着关闭它或降低那个版本的质量,而只是告诉客户旧版本将会在未来的某一天不能使用,需要他们尽快更新到最新的版本。
好的RESTful API会在URL上显示版本号。最常见的解决办法就是把版本号放在请求的头部中。但经过我跟很多不同的开发人员的讨论,得出的结论是将版本号放在HTTP头信息中不如放在URL上来的方便和直观。例:将API的版本号放入URL:
https://api.example.com/v1/
5. 分析 (Analytics)
优秀的程序会经常追踪客户调用你的API的版本/路径。这个可以在你的数据库中用整数型字段统计下,当每次请求过来的时候,就增加一次。这样做有很多优点,最常见的好处是我们就可以知道哪个API请求是最高效的。
为了创建第三方开发人员喜欢的API,最重要的事情是当你不赞成使用旧的API版本时,你需要联系那些还在使用旧版本的开发人员。这可能是你升级并彻底放弃一个API旧版本最好的方法。
通知第三方开发人员可以是自动的,例如当超过10,000的不赞成使用的请求被调用时,就可以自动发邮件给那些开发人员。
6. API根路径 (API Root URL)
你的API根路径(域名)的设计非常重要。当一个开发人员(可看作是作古派风格的开发者)在一个很老的程序上使用你的API的新的特性时,他们可能不知道你服务器,也许他们可能只是知道一系列的后缀请求的URL。所以尽量简化你的API根路径,因为一个复杂的URL会使得一些开发人员放弃你的API。
以下是两个常见的根路径URL:
https://example.org/api/v1/*
https://api.example.com/v1/*
如果你的应用很大或是你预期你的应用会很大,那么把你的API放到单独的子域里是个很不错的选择,这样可以使得你的API更加的灵活容易维护。例如:https://api.example.com/v1/*
如果你觉得你的API不是很复杂,或者是你只想要个小应用程序(比如你希望你的网站和API放在同一个框架下),那么就把你的API放在根域名的URL后面。例如:https://example.org/api/v1/*
有个可以存放API路径列表的页面是个很好的主意。比如点击GitHub的API的根路径就可以得到一个路径列表。就我个人而言,我是非常喜欢这种点击根路径就得到所有API URL或者得到API的开发文档的形式,这样有助于开发者的开发。
另外,作为好的RESTful API, API与用户的通信协议,总是使用HTTPs协议。
7. 路径 (Endpoint)
路径就是你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动作的路径。以下列举了一些可行的动作来代表一个可以运作的API,值得注意的是我已经把每个HTTP动作放到了路径前,并写上了一些注释。
- GET /zoos: 列出所有的动物园(返回ID和名称,没有其它更详细的资料。)
- POST /zoos: 新建一个动物园
- GET /zoos/ZID: 根据ZID取出一整个动物园的对象
- PUT /zoos/ZID: 更新一个动物园(传入全部属性以及ZID)
- PATCH /zoos/ZID: 更新一个动物园(传入要更新的属性以及ZID)
- DELETE /zoos/ZID: 删除一个动物园
- GET /zoos/ZID/animals: 根据ZID取出动物的列表(返回动物的ID和名称)
- GET /animals: 列举出所有的动物。(返回动物ID和名称)
- POST /animals: 新建一个动物。
- GET /animals/AID: 根据动物的ID取出相应的动物的对象。
- PUT /animals/AID: 新增一个动物(传入全部属性以及AID)
- PATCH /animals/AID: 新增一个动物(传入要更新的属性以及AID)
- GET /animal_types: 根据传入的动物园类型获取动物园的列表(返回动物园ID和名称)
- GET /animal_types/ATID: 根据传入的动物园类型ID获取一个动物园的信息
- GET /employees: 获取所有的员工列表
- GET /employees/EID: 根据员工的ID获取员工信息
- GET /zoos/ZID/employees: 获取在指定动物园里工作的员工列表(返回员工ID和姓名)
- POST /employees: 新增一个员工
- POST /zoos/ZID/employees: 在指定的动物园里雇佣一个员工
- DELETE /zoos/ZID/employees/EID: 在指定的动画园里解雇一个员工
上述列表中,ZID表示动物园ID,AID表示动物ID,EID表示员工ID,ATID表示动物类型ID。在你的文档中标明是简写意思一个很好的习惯。
我简化了以上这些常用的API URL前缀。这个形式有利于开发时的交流,当然在你正式的API文档中,你需要补齐全部URL的路径。(例如:GET http://api.example.com/v1/animal_type/ATID
).
值得注意的是关于员工和动物园的数据间,可以有很多不同的展示及联系,下面我会列举一个URL,来说明下它们之间特殊的相互影响。比如当我们想解雇一个员工时,我们没有一个HTTP动作叫‘FIRE’,但是我们可以通过HTTP动作‘DELETE’来表示员工的解雇,这样也达到了相同的目的。
8. 信息过滤 (Filtering)
当一个客户发来获取一个对象的列表请求时,服务器所给他的每一个对象是否与请求的标准相匹配是非常重要的。如果这个列表的记录数量很多。那么很重要的一点就是服务器不能将它们都返回给用户。不然这么庞大的数据量会使得第三方开发人员很难去分析获得的数据。如果他们请求的是一个确定的集合,然后迭代得到结果,那么他们就不会想要再看到另外多出来的那100条数据。否则他们就要花额外的工作量去分析那多出来的数据。问题是他们的程序去要去做一些限制呢?还是任由这样的事情发生而不去管呢?
最好的办法是API提供一些参数,让第三方开发人员传参去限制。
通过传参的模式,服务器可以为客户提供一些对结果集的排序等功能。当然最重要的是限定参数的办法使得网络负担变小了,同时客户也能准确的得到他们想要的数据。此外,客户也可能是比较懒惰的,如果客户端能为他们提供过滤或是分页的数据,那何乐而不为呢?
信息过滤最常见是存在于HTTP动作的GET中,既然他们是GET请求,那么过滤参数就必须写在URL里。下面是一些你可以参考的关于过滤参数的例子:
- ?limit=10: 指定返回记录的数量 (有分页)
- ?offset=10: 返回记录的开始位置 (有分页)
- ?animal_type_id=1: 指定筛选条件。(例如WHERE animal_type_id = 1)
- ?sortby=name&order=asc: 指定返回结果按照哪个属性排序,以及排序顺序。 (例如ORDER BY name ASC)
对于API的路径和URL的参数允许存在冗余(即重复)。比如我之前的例子GET/zoo/ZID/animals
和GET /animals?zoo_id=ZID
的含义就是相同的。有些专用的路径会方便客户的调用,特别是那些你预计他们会经常使用的路径。在你的API文档中,需要写清楚哪些是冗余的,这样可以消除第三方开发人员的疑惑。
另外还要提一下,当参数可以指定按照某一属性进行排序时,你需要指出哪些属性是可以排序的,即需要整理出可以进行排序的属性的白名单,因为我们不需要发送错误的数据给客户。
9. 状态码 (Status Codes)
作为一个RESTful的API,你需要正确使用HTTP的响应码。他们是一套标准,许多网络技术、设备都能读懂。例如负载均衡(load balancers)就能分辩出这些状态码并且避免了当错误为50开头时发送给浏览器的很多回应。HTTP有很多很状态码,查看全部请点击这里,下面我简单列了一些重要的状态码:
- 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
- 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
- 204 NO CONTENT - [DELETE]:用户删除数据成功。
- 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
- 404 NOT FOUND - :用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
- 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
状态码边界:
- 1XX状态码是HTTP比较低等级的状态码,在HTTP/1.0协议中没有定议任何1XX状态码,所以最好不要向客户发送任何1XX的状态码。
- 2XX状态码说明了客户端接收了正确的消息。这种状态码是极好的,所以确保你的服务器发送的都是这类的状态码。
- 3XX状态码用来重定向。很多API通常不会使用这些请求(SEO优化人员会使用它们)。值得一提的是,新出来的Hypermedia API还是会使用3XX的状态码的。
- 4XX状态码表示客户端的请求错误。比如客户端提供了错误的参数或请求要获取的不存在的对象。对于客户端和服务器端来说这些请求是等幂的。
- 5XX状态码一般是服务器在处理请求过程中的异常。一般来说这些错误被低等级的方法所抛出,当然也不排除开发人员手动发送,来确保某个客户得到某种回应。当服务器发送了5XX状态码,这个客户可能都不会收到任何回应,所以这些错误也是不可避免的。
10. 文档返回值 (Expected Return Documents)
当使用不同的HTTP动作来获取资源时,一个用户需要获取一些有排序的数据,以下列举了典型的RESTful APIs:
- GET /collection: 返回一个资源集合。
- GET /collection/resource: 返回单个资源对象。
- POST /collection: 返回最新创建的资源对象。
- PUT /collection/resource: 返回更新完毕的资源对象。
- PATCH /collection/resource: 返回更新完毕的资源对象。
- DELETE /collection/resource: 返回一个空的文档。
当客户新增一个资源时,他们通常不知道新增的资源的ID(或最新更新的时间等属性),这些额外的属性需要通过POST同时返回给客户。
11. 身份认证 (Authentication)
大多数情况下服务器会对请求方的身份进行验证。当然有些API提供了可以被普通用户调用的路径,但多数的API都需要认证的过程。
OAuth2.0提供了一个很好的解决方案。在每次请求时,服务器需要知道哪个第三方程序发来想要获取系统的某个用户信息的请求,这时候,就需要用户授权给那个第三方程序,允许它获得用户的信息。
除此之外,还有OAuth 1.0 和 xAuth。无论你用的是哪种认证技术,你都需要确保你所使用的是常见技术,然后编辑好文档和供不同平台使用的类库。
就我的经验来看,关于以安全著称的OAuth1.0a,对它对接接口是非常痛苦的事。我最近惊奇的发现由于它没有提供很多语言的类型,很多的第三方开发人员都开始拥有了自己的类库。我曾经为了它用了好几个小时来调试一个叫“无效签名”的错误,这真是太糟糕了。
12. 内容形式 (Content Type)
关于服务器返回的数据格式,应该尽量使用JSON格式。包括Facebook, Twitter, GitHub都是使用了这种格式。其它的格式还有XML、SOAP。但是XML格式容易引起很多纠纷(特别是大型交互环境)。SOAP在当今几乎没人用了。
开发人员使用流行的语言我框架可以对数据进行有效的转换。如果服务器想提供一个很普通的回应对象,那么它可以很容易的提供上述的数据格式(不包括SOAP)。值得注意的是,你需要正确使用响应文档中的头部内容。
一些API创建者在响应内容中提供了响应格式的参数,包括json\xml\html。虽然本人很少这样干,但仍然觉得这是个不错的形式。
13. 超媒体 (Hypermedia APIs)
Hypermedia APIs是RESTful API设计的发展趋势,即在返回结果中提供链接。这样可以使用户在不查API文 档的前提下,也可能知道下一步应该要做什么。
当使用一个不是Hypermedia的RESTful API时,路径是服务器和客户之间的联系。但这些路径必须是被客户知道的,一旦服务器改变了这些路径的作用,那么客户将不能跟服务器进行通信,这就是传统RESTful API的一大限制。
对于现在来说,API客户不再是发起HTTP的唯一请求方,用着浏览器的人们都是发起HTTP的请求方。但是这些人并不会局限于使用预先定定好的RESTful API。这是为什么呢?因为他们读的是真正的内容,他们点击一些想要读的标题,然后一个网页就会打开来了,他们就可以知道他们真正想要的内容。如果一个URL改变了,网友们是不会受影响的(除非他们定阅了一个页面,而不是在首页是新打开一个标题去阅读)
Hypermedia API的概念跟现实人类的世界很像。请求API的根目录得到可以请求每个集合信息以及描述这些集合的用途的URL列表。另外,提供给每个资原的ID不是必须的,因为URL里已经提供。
当一个用户使用Hypemedia风格的API的链接来收集信息时,响应里一般都会包含最新的URL,这些URL没必要提前写在文档里。如果一个URL被缓存了,那么可能就会返回404错误,客户可以很容易的回到根目录来找到正确的URL。
当要获取一个资源列表的集合,可以在响应的资源里放上一个关于URL的属性。当需要调用POST/PATCH/PUT,服务器回应的时候可以直接用3XX的响应码来做重定向。
14. 文档 (Documentation)
说实话,如果你不是100%确定你的标准,那么你的API也不是最糟糕的。但如果你的API没有文 档,这将会是很糟糕的API,那么没有人将会使用你的API。
所以撰写API文档是十分有必要的。
不要使用自动生成的文档,但如果你真的这么做了,那一定要保证你检查和校验过这个文档。
不要缩减请求和响应的内容,最好就全部展求。在你的文档中有些重点的要加粗。
在文档中,要加上每个路径的响应的内容和可能出现的信息错误,并尽可能的写上在什么情况下可能导致这些错误。
如果你有时间的话,就建立一个开发者API的终端机,这样开发者就可以更方便 的体验你的API。这对你来说其实并不困难,并且所有的开发者(你公司内部的或第三方开发者)都会很喜欢这种形式。
必须得确保你的文档可以被打印。CSS是个很强大的技术。当要打印的时候不必要隐藏你的侧边栏,因为很多开发者都喜欢离线复印去阅读API的文档。
15. 其它:HTTP包文
既然我们所做的所有事都是基于HTTP,那么我们接下来要讨论下HTTP包文。如果一个从事这个工作的人不知道HTTP包文,那是很糟糕的事情。当一个客户发送给服务器发送请求,会提供一个包含键/值(Key/Value)的集合,叫头部(Header),再过几行就是请求的内容(Body)了。头部和内容是放在同一个包里进行发送的。
服务器的响应格式也是一对对的键/值(Key/Value)形式,HTTP是一个请求/响应的协议,它不支持服务器无缘无故的主动推送给客户端,除非你使用另一个叫Websockets的协议。
当设计API时,一般需要用一些工具来帮助你看HTTP的包。比如Wireshark,当然你也可以用一些框架/WEB的服务自带的工具来确保
例子: HTTP的请求
POST /v1/animal HTTP/1.1
Host: api.example.org
Accept: application/json
Content-Type: application/json
Content-Length: 24
{
"name": "Gir",
"animal_type": 12
}
例子: HTTP的响应
HTTP/1.1 200 OK
Date: Wed, 18 Dec 2013 06:08:22 GMT
Content-Type: application/json
Access-Control-Max-Age: 1728000
Cache-Control: no-cache
{
"id": 12,
"created": 1386363036,
"modified": 1386363036,
"name": "Gir",
"animal_type": 12
}
附录1 API Action
1.1 Create Action
API | Name | Desc |
---|---|---|
Create | 新增 | C |
BatchCreate | 批量新增 | C |
BatchImport | 批量导入 | C |
1.2 Retrieve Action
API | Name | Desc |
---|---|---|
GetAll | 获取全部,不分页的情况 | R |
GetPage | 获取分页 | R |
GetDetail | 获取单条 | R |
GetTree | 获取全部记录以树形方式返回,不分页的情况 | R |
1.3 Update Action
API | Name | Desc |
---|---|---|
Update | 单条编辑 | U |
BatchUpdate | 批量编辑 | U |
1.4 Delete Action
API | Name | Desc |
---|---|---|
Delete | 单条删除 | U |
BatchDelete | 批量删除 | U |
BatchClear | 按照某一特定条件批量删除 | U |
1.5 Other Action
API | Name | Desc |
---|---|---|
Login | 登录 | R |
Register | 注册 | U |
ChangePwd | 修改密码 | U |
Save | 插入或者修改 | CU |