REST API开发技巧和最佳实践(二)

模式

尽管你的API代码超级干净和亮眼,但你的客户可能永远看不到它。他们对你的API架构并不感冒。

您应该尝试使用清晰的能解释的名称和尽可能接近文档要求的架构。此外,通过一些简单的规则,您应该能够对其进行以后的扩展,以便可以继续向现有响应中添加更多信息,而不必早早的淘汰旧版本。

1. 模式实施建议

几年来,我遇到的最常见的与模式相关的错误就是由于API开发环境缺乏严格性。在设计API时,使用适合您的编程语言的框架对您将要做的事情大有裨益。

花些时间研究可用的选项,并记住,尽管大多数Web框架提供了可帮助您实现REST API的工具,但仍需要您自己完成大量的工作,永远不要重复造轮子。

保持严格的数据模型层

最流行的后端Web技术(也包括前端)是基于松散类型的脚本编程语言,这些语言具有不良的编程习惯,这些习惯很难摆脱。PHP和Javascript(以Node.js的形式)是数据类型松散型的典型,因为它们经常在没有严格的数据模型下使用。

确保为您的数据库使用ORM或其他数据模型基础结构严格建模的内部数据层。您的API响应应该被包含在严格定义的嵌套数据中。不要将数据库查询的结果生成简单的数据结构。无论怎样,保持严格的数据层是您在所有应用程序中应该做的。

在暴露给API响应之前,应始终对数据进行过滤,以免泄露敏感信息。对暴露给API的字段所做的任何修改,都应该注意与当前API版本的兼容性,并将其添加到API的更新日志中

2. 良好的架构实践

统一命名

对代码的命名应该在整个API,URI以及请求和响应中保持一致,并且具有特殊的意义。命名不明确会给其他开发人员以及您自己造成很大的压力。

请记住,您的URI和架构应尽可能清晰地向使用者传达目的,而无需他们不断的浏览文档。不要害怕使用冗长的名称。

严格的数据类型

即使您用的编程语言不严格,您也应该使用严格的数据类型。数字字段永远应该只有数字,字符串字段永远应该只包含字符串,诸如子类。您永远都不应该在相同的字段中混合不同的数据类型。您的字段对应的值有可能会不一样,但该字段的额数据类型应该确定。

在松散类型的环境中看到同一个字段在一个响应中是数字42,但在另一个响应中是字符串“42”是很常见的错误。这个做法前后不一,很难再所有的客户端安全解析。在松散类型的架构中,客户在解析每个字段时都很危险。

显然,这不仅使用与原始数据类型(数字,字符串,布尔值等),而且使用于JSON对象和数组。请勿在包含Table类型的对象字段中返回Chair类型的对象,或在包含Bicycles数组的字段中返回Cars数组。

虽然上面说的都是非常基础的知识,但很多优秀的开发人员都犯过类似的错误。

一个强壮的模型层可帮助您避免此类尴尬的错误。

不要忽略字段

当某个字段没有可用值时,请勿完全忽略该字段。根据数据类型和缺失值的语义,使用null,空字符串,空数组或零。

当一个接口满足要求时,不要使用10个接口来获取所有的字段。也许他们可以在文档中查找它,但为什么不能让文档更容易理解呢?请记住,文档变得陈旧比代码快得多,而代码才是生成架构的原因。

再一次强调,如果在实现中使用强大的模型层,则会更容易避免这种类型的错误。

不要滥用JSON对象

我见过的次数超过了我在API请求和响应中记得的次数,这也通常源于与松散类型语言相关的不良做法。

假设您有一个包含唯一id作为主键和子对象作为值的people对象

{
    "people": {
        "1234": {
            "name": "John",
            "surname": "Smith"
        },
        "5678": {
            "name ": "John ",
            "surname": "Smith"
        }
    }
}

people对象随着它内部的内容数据类型的变化而变化,在这个例子中,它的key值是1234和5678,但是没有人知道下一次请求时它的key值是什么。

这是个很可怕的做法,并且在任何严格类型的语言中进行解析时,都会导致代码不一致而普遍糟糕。API中的每个JSON对象在请求时都应始终具有一组不变的严格定义的字段。

下面的是一个很好的数组用例,只需返回一个数组,并将id包含在每个数组元素中即可。

{
    "people": [
        {
            "id": 1234,
            "name": "John",
            "surname": "Smith"
        },
        {
            "id": 5678,
            "name": "John",
            "surname": "Smith"
        }
    ]
}

通常,当您尝试通过使用带有唯一ID键的字段来使后端代码的查找更容易时,就会出现JSON滥用。您必须谨记,在这种情况下,内部实现的详细信息会泄露给用户-在软件开发的所有方面都应避免这种现象。

不要滥用JSON数组

如果您遵循了先前的建议,并将某些对象更改为数组,那你做的很好!

现在,您必须确保数组仅包含一种类型的对象。不要将apples和oranges混合一起! 请记住,并非所有的客户端都是用松散类型的容器来存储数据,并且解析异构资源列表不仅不一致且令人讨厌,而且也不安全。

当你确实无法避免在同一数组中返回不同种类的实体时,请尝试返回一个超级对象列表,这些列表足够抽象以描述您需要返回的所有对象类型的属性。

在苹果和橙子的例子中,或许你应该返回一个Fruit对象,一个Fruit对象可以包含Apple和Organge对象的所有属性,以及一个字段,该字段可以准确描述每个对象的水果类型。

如果您返回的项的属性对于每种返回的类型都是完全不同的,但仍必须将它们返回到同一列表中,则可能必须使用极端措施,比如容器对象。虽然不是非常优雅的解决方案,但这也是必须的。

下面是容器对象的例子:

你的接口将返回一个人拥有的飞行器的列表,以及每个飞行器的一些基本特征。飞行器可以是飞机,也可以是热气球,两者之间有很大的不同。从语义上讲,向热气球添加翼展,引擎数量或马力等属性几乎没有意义,向飞机添加蓝,气球材质和气球形状等属性也没有意义。

将所有这些属性字段添加到单个对象类型是毫无意义的。相反,您可以将飞机对象和热气球对象存储在一个容器对象中。

在这种情况下,类型为Vehicle(本质上是超类型)的容器对象将包含两个字段airplane和ballon,分别对应不同的子对象。 请记住,即使其中的一个字段没有数据,也要将该字段及其数据类型返回。

{
    "vehicles": [
        {
            "type": "airplane",
            "airplane": {
                "engines": 1,
                "wing\_span": 12,
                "horsepower": 240
            },
             "balloon": null
        },
        {
            "type": "balloon",
            "airplane": null,
            "balloon": {
                "basket": "rattan",
                "balloon\_material": "dacron",
                "balloon\_shape": "natural"
            }
        }
    ]
}

再次强调,如果可能的话,请避开这种设计,但是如果您必须在同一集合中返回完全不同的对象,则容器对象是一种维护严格类型的架构的好方法。

不要依赖简单的硬编码错误消息

我不想让您失望,但是无论您的错误消息多么有趣和风趣,它们几乎都不会引起用户的兴趣。并不是其他开发人员不欣赏您的写作技巧,而是你永远不知道客户将怎样呈现一个错误。

此外,您必须始终以简洁,机器可读的方式返回错误,以使其被客户端易于解析。您应该返回正确的HTTP状态代码,并在相应正文中的错误对象中包含特定的错误消息。

下面是一个例子:

您的用户通过下面的API请求一个订单

GET /customers/21/order/42

如果未找到客户或订单,则响应404 Not Found状态码,但这样就行了吗?用户将无法准确的区分导致错误的原因,因为他不知道错误的原因是客户还是订单。

这就是您的响应正文中的错误对象派上用场的地方。

一个可读的机器码使事情对客户端而言更简单。此外,将说明(例如:“customer_not_found”)而不是数字,可以使开发人员更轻松-无需查看API文档中的数字表和错误说明。

最后,message 字段可以更好地向开发人员解释错误的原因,因此他们对如何处理错误以及在何处查找其他信息有了更好的了解。 理想情况下,应根据客户请求的“接受语言”标头对错误进行本地化。 谁知道呢,也许最终用户会在某种程度上阅读您的杰作。

不要使用数字枚举

正如一遍又一遍提到的,您应该有一个易于阅读和自我记录的架构。 枚举时请勿使用数字。 使用简单的字符串。

您的动物对象中有一个类型字段吗? 请勿使用1、2、3、4和5作为其值。“ dog”,“ cat”,“ parrot”,“ armadillo”和“ elephant”更容易被人阅读,并且对 知道如何比较字符串的机器。

人们通常在后端内部使用数字枚举时执行此操作,但这(同样)是一种实现细节,不应泄漏给API使用者。

我还听到了一些借口,例如增加字符串方法的带宽消耗,但是还有其他更好的方法来解决该问题。 您的架构应足够详细,以便一眼就能理解,并且应该使用Gzip减少带宽消耗,与使用数字枚举节省几个字节相比,Gzip具有很大的不同。

不要返回未封装的JSON数组

在这种情况下,封装(或JSON)到底是什么?简而言之,这意味着将响应数据包装(或封装)到JSON对象中,然后将其返回到响应主体根目录中的data(或其他类似名称)字段中。

某些人似乎认为这是对所有响应的一种好习惯,因为它允许您将来添加元数据字段(例如错误或分页信息),而不会篡改主要响应对象。尽管在解析时可能需要更多的代码,但这确实使API模式更加简洁。

即使您不想对所有响应都这样做,但我相信当您返回对象集合时,它非常有用(甚至有必要)。在这种情况下,您永远不要将数组作为响应的根容器!

上面的主要理由是,如果您的根容器是JSON数组,则在响应需要返回错误(不可避免地将是JSON对象)时,您的架构会发生根本性的变化。这使得解析更加复杂,而没有提供任何实际好处。

此外,(即使您不理会上述情况),数组也使早期弃用API的可能性更大,因为在不弃用架构的情况下,绝不能以任何方式对其进行更改或修改。另一方面,将对象用作根响应容器可让您以后添加任意多个字段,而不会引起弃用。哎呀,您甚至可以在新数组中返回不同的更新类型的对象,只要您确保保留旧数组即可。

使用Unix时间戳或ISO-8601日期

我个人的喜好始终是将Unix时间戳作为响应中的日期,因为它们相对较短并且很容易解析。 但是,除非您是机器,否则它们很难转换,并且实际上只能作为真实日期来读取。 另一方面,从可读性的角度来看,ISO-8601日期更好,但解析起来却有点困难(尽管不多)。

应该不惜一切代价避免使用除这两种以外的任何其他字符串格式,因为它可能在解析时造成歧义。 我知道您可以在文档中指定自己的日期时间格式,客户端可以基于此格式解析日期,但是请记住:在没有太多外部帮助的情况下,API应该尽可能易于理解。

避免填充大量的对象

如果对象A不是用户; 并且包含诸如user_id,user_name,user_favorite_color,user_pet等字段,也许是时候在对象A内使用封装的User对象了……

由于(数据库以及API)架构会随着时间的流逝而变得越来越复杂,因此最好一开始就对其进行规范化并使其保持尽可能的干净。

将对象用于将来可能需要更多信息的字段

始终尽力以可能的方式使API面向未来。尝试保留将来很可能需要其他信息的属性,以便可以延长主版本的使用寿命。

您需要先考虑一下:

假设您每个Book对象都有一个is_available布尔值。尽管这足以让我们知道一本书为假时该书不可用,但它并没有告诉我们为什么该书不可用或何时可以再次使用。将来,如果要添加该信息,则必须通过添加两个额外的字段来加入到Book对象。

一种更干净的方法是使用一个可用性字段,该字段存储一个Availability对象,该对象最初仅包含is_available字段,但可以进行修改以包含有关该书的可用性的其他信息(例如,该书不可用的原因以及何时将其再次可用的时间戳记),而不会在原来的架构中添加更多字段。

3. 模式启用

如果您使用的是版本控制方法,可在保证结构稳定的同时对API进行小的增量更改,但是每次更改时都应格外小心。

对结构的某些更改意味着立即弃用当前的主版本。 除非绝对必要,否则应避免使用它们。

请记住,下面列出的更改不是架构弃用的唯一原因(通常可以由您特定设计中的细节引起),而只是最常见的原因。

不要删除字段或更改字段名称

您永远无法确定您的用户如何使用您的信息。不管字段看起来多么微不足道或多余,如果您将其交付生产时都犯了错误,那么您将一直坚持到下一个主要版本。更改字段名称显然与删除字段相同。

不要更改数据类型

数据类型不仅必须在响应中保持严格,而且在次要版本中也必须保持严格。这是与松散类型的开发环境有关的另一种不好的做法。

您必须更改当前版本才能更改字段的数据类型。如果您在初次发布时的响应中将数字作为字符串传递,那么在下一个主要版本之前,应始终将其作为字符串返回。

不要编辑现有的枚举案例

返回数字(请记住,您不应该这样)或字符串是枚举形式,这很常见。

例如,您可能使用以下情况:“ car”,“ truck”和“ motrcycle”字段用于vehicle_type字段。
如果您注意到“摩托车周期”中的错字,我知道您一定感到沮丧,但您无法解决!不过,您可以在下一个主要版本中进行操作(并且不要忘记将其添加到变更日志中)。

你可能感兴趣的