REST API 设计最佳实践:为什么不要在URI中使用动词?
总的来说,HTTP协议出现以来Web服务也就存在了。但是,自从云计算出现后,才成为实现客户端与服务和数据交互的普遍方法。
作为一名开发者,我很幸运能够在工作中使用一些仍然存在的SOAP服务。但是,我主要接触的是REST,这是一种基于资源的API和Web服务开发架构风格。在我的职业生涯中有很大一部分时间都参与了构建、设计和使用API 的项目。我见过的大多数API 都“声称” 是 “符合REST原则”的——意味着遵循 REST 架构的原则和约束。但是,我也曾遇到过一些让 REST 蒙羞的 API 例子,错误使用 HTTP 状态码、纯文本响应、不一致的模式、插入端点中动词...
因此我决定写篇文章分享一下,在设计 REST API 时的最佳实践。以下是关于设计优秀REST API 的一些建议、提示和指导,帮助您让消费者(以及开发人员)满意。
1. 学习 HTTP 基础知识
如果你想构建一个设计良好的REST API,那么你必须了解HTTP协议的基本知识。我坚信这将帮助你做出正确的设计选择。Mozilla Developer Network文档上关于HTTP概述是一个相当全面的参考资料,尽管如此,在REST API设计方面,以下是将HTTP应用于RESTful设计的简要说明:
-
HTTP具有动词(操作或方法):最常见的是GET、POST、PUT、PATCH和DELETE。
-
REST以资源为导向,资源由URI表示:
/library/
-
端点(endpoint)是动词和URI的组合,例如:
GET: /books/
-
端点可以理解为对资源执行的操作。例如:
POST: /books/
可能意味着“创建一本新书”。 -
高一层次来看,动词映射到CRUD操作:
GET表示读取,POST表示创建,PUT和PATCH表示更新,DELETE表示删除
-
响应状态由其状态码指定:
1xx 表示信息, 2xx 表示成功, 3xx 表示重定向, 4xx 表示客户端错误 和5xx 表示服务器错误
当然你还可以使用其他 HTTP 协议提供给 REST API 设计的功能 ,但这些都必须牢记在心里。
2. 不要返回纯文本
尽管并非强制规定的,但大多数REST API通常约定使用JSON作为数据格式。然而,仅返回包含JSON格式字符串的响应体是不够好的。您还应该指定Content-Type
标头。它必须设置为application/json
值。
在处理应用程序/编程客户端(例如,通过Python中的requests
库与您的API交互的另一个服务/API)时,这一点尤为重要——其中一些客户端依赖于此标头来准确解码响应。
3. 不要在 URI 中使用动词
到目前为止,如果您已经理解了基本概念,那么您会开始意识到在URI中放置动词是不符合RESTful的,这是因为HTTP动词应该足以准确描述正在对资源执行的操作。
示例:假设您要提供一个端点来生成和检索一本书的封面。我将注意到:param
是一个URI参数(如ID或缩写)的占位符,你第一个想法可能是创建类似于这个的端点:
GET: /books/:slug/generateBookCover/
但是,在这里GET方法在语法上足以说明我们正在获取(“GET”)一本书的封面。所以,让我们只使用:
GET: /books/:slug/bookCover/
同样,对于创建新书的端点:
#Don’t do this
POST: /books/createNewBook/
#Do this
POST: /books/
4. 使用复数名词表示资源
我们应该使用 /book/:id/ (单数) 还是 /books/:id/ (复数)?我个人建议使用复数形式。为什么?因为它非常适合所有类型的端点。
我可以看到 GET /book/2/ 是没问题的。但是 GET /book/ 呢?我们是在获取图书馆里唯一的那本书、其中几本还是全部?为了避免这种模棱两可的情况,让我们保持一致(💡软件职业建议!)并在所有地方都使用复数:
GET: /books/2/
POST: /books/
...
5. 在响应体中返回错误详情
当API服务器处理错误时,将错误详细信息包含在JSON主体中可以帮助使用者进行调试,这是是非常方便的,如果您还能说明哪些字段受到了错误的影响,那就更好了!
{
"error": "Invalid payload.",
"detail": {
"name": "This field is required."
}
}
6. 特别关注 HTTP 状态码
这一点非常重要,如果你从这篇文章中只记住一件事,那可能就是它了。
你的API最糟糕的事情莫过于返回一个带有
200 OK
状态码的错误响应。
这是最差的语义,相反,应该返回一个能准确描述错误类型的有意义HTTP状态码。尽管如此,你可能还在想:“但我按照您推荐的方式,在响应体中发送了错误详细信息,那么问题出在哪里呢?”
让我给你讲个故事吧。曾经我不得不集成一个API,它对每个响应都返回200 OK
,并通过status
字段来表示请求是否成功:
{
"status": "success",
"data": {}
}
尽管HTTP状态码返回200 OK
,但我不能完全确定它有没有处理我的请求失败。
实际上,API可以返回如下响应:
HTTP/1.1 200 OK
Content-Type: text/html{
"status": "failure",
"data": {
"error": "Expected at least three items in the list."
}
}
因此,我必须检查状态代码和临时状态字段,以确保一切正常后才能读取数据。太烦人了!
这种设计真的很糟糕,因为它破坏了API与其使用者之间的信任关系,你会担心API可能在欺骗你。所有这些都极不符合RESTful风格。那么你应该怎么做呢?利用HTTP状态码,并且只在响应体中提供错误详细信息。
HTTP/1.1 400 Bad Request
Content-Type: application/json{
"error": "Expected at least three items in the list."
}
7. 你应该始终保持一致地使用 HTTP 状态码
一旦你掌握了HTTP状态码,就应该力求始终如一地使用它们。例如,如果你选择某个POST
端点返回201 Created
,那么对于每个POST
端点都应使用相同的HTTP状态码。为什么?因为消费者不应该担心在哪种情况下哪个方法在哪个端点上会返回哪个状态码。
所以,请保持可预测性(一致性)。如果必须偏离约定,请在某处用大标志记录下来。通常,我遵循以下模式:
GET: 200 OK
PUT: 200 OK
POST: 201 Created
PATCH: 200 OK
DELETE: 204 No Content
8. 不要嵌套资源
您可能已经注意到,REST API处理的是资源。检索资源列表或单个实例非常简单,但是,当处理相关资源时会发生什么呢?例如,假设我们想要检索特定作者(名为Cagan)的书籍列表。基本上有两个选择。
第一个选项是将books资源嵌套在authors资源下面,例如:
GET: /authors/Cagan/books/
一些架构师推荐这种约定,因为它确实表示了作者与其书籍之间的一对多关系。但是,现在不再清楚您请求的是哪种类型的资源。 是作者吗?还是书籍?...而且扁平化总比嵌套好,所以肯定有更好的方法... 确实如此!我个人建议使用查询字符串参数直接过滤books资源:
GET: /books?author=Cagan
这显然意味着:“获取所有名为Cagan 的作者所写的书”,对吧。
9. 优雅地处理尾部斜杠
关于URI是否应该有尾随斜杠/
实际上并不是一个值得争论的问题,你只需要选择其中一种方式(即带或不带尾随斜杠),坚持使用它,并在客户端使用错误约定时优雅地重定向。
讲个故事吧! 有一天,当我将REST API集成到我的一个项目中时,每次调用都收到HTTP 500内部错误
。我所使用的端点看起来像这样:
POST: /buckets
当时我非常生气,怎么也想不明白究竟哪里出了问题。最后,原来是因为缺少了尾随斜杠导致服务器出错!于是,我开始使用:
POST: /buckets/
然后一切都顺利进行了。API没有修复,但希望您可以防止消费者遇到此类问题。专业提示:大多数基于网络的框架(Angular、React等)都有一个选项可以优雅地重定向至带或不带尾随斜杠的URL版本。找到那个选项并尽早激活。
10. 利用查询字符串进行筛选和分页
大多数情况下,一个简单的端点无法满足各种复杂的业务场景。您的用户可能希望检索满足特定条件的项目,或者一次只检索少量数据以提高性能,这正是过滤和分页功能所设计的目标。
通过过滤,消费者可以指定返回项目应具有哪些参数(或属性)。分页允许用户逐步获取数据集。最简单类型的分页就是按页码进行分页,它由page
和page size
确定。现在问题来了:如何将这样的功能融入REST API?
我的答案是:使用查询字符串(querystring)。
我认为使用查询字符串实现分页非常明显。它看起来像这样:
GET: /books?page=1&page_size=10
但对于过滤来说可能不那么明显。首先,你可能会想做类似以下操作以仅检索已发布书籍列表:
GET: /books/published/
设计问题:published
不是资源!相反,它是您要检索数据所具备特征。此类内容应放在查询字符串中。因此最后, 用户可以像这样获取“包含20个项目、已发布书籍第二页”:
GET: /books?published=true&page=2&page_size=10
美观且清晰易懂,不是吗?
11. 了解401未授权和403禁止之间的区别
如果我每看到一次开发人员甚至有经验的架构师搞砸这个问题就能得到一个25美分硬币……在处理REST API中的安全错误时,很容易弄混错误是与身份验证还是授权(又称权限)相关 - 我以前总是遇到这种情况。根据不同情况,以下是我的备忘单,用于了解我正在处理什么问题:
-
消费者没有提供身份验证凭据吗?他们的SSO令牌是否无效/超时?👉 401 未授权。
-
消费者正确地进行了身份验证,但他们没有访问资源所需的权限/适当的许可吗?👉 403 禁止。
12. 充分利用 HTTP 202 Accepted
我认为202 Accepted
是一个非常方便的替代201 Created的选项。它基本上意味着:
我,服务器,已经理解了你的请求。虽然我还没有创建资源(尚未),但这没问题。
有两个主要场景,我觉得202 Accepted
特别适用:
-
如果资源将在未来处理后被创建 — 例如:在某个工作/流程完成之后。
-
如果资源以某种方式已经存在,但这不应被视为错误。
13. 使用专门针对REST API的网络框架
作为最后一个最佳实践,让我们讨论这个问题:如何在您的API中实际应用最佳实践?大多数时候,您希望建立一个快速的API,以便一些服务可以相互交互。Python开发者会选择Flask,JavaScript开发者会选择Node(Express),然后他们会实现一些简单的路由来处理HTTP请求。
这种方法的问题在于,通常情况下,框架并不是针对构建REST API服务器而设计的。例如,Flask和Express都是两个非常灵活的框架,但它们并没有专门为帮助您构建REST API而制定。因此,在API中应用最佳实践需要采取额外措施。而且大多数时候, 懒惰或缺乏时间意味着你不会付出努力——从而使你的消费者面临一个古怪的API。
解决方案很简单:使用合适工具完成任务。
各种语言中已经出现了新框架, 它们专门用于构建REST APIs。它们能够帮助您轻松遵循最佳做法,并提高生产力。
在Python中, 我找到过其中之一优秀API框架就是Falcon。它与Flask一样简单易用,速度很快,非常适合在几分钟内构建REST API。
如果您更喜欢使用Django,那么首选就是Django REST框架。虽然它不如其他框架直观,但功能非常强大。在Node中,Restify似乎也是一个很好的选择,尽管我还没有尝试过。我强烈建议您试一试这些框架,它们将帮助您构建美观、优雅且设计精良的REST API。
结束语
我们都应该努力使API变得易于使用。无论是对于消费者,还是我们自己的开发人员同伴。我希望这篇文章能帮助你学到一些技巧,并激发出构建更好REST API的方法。对我来说,这只是归结为良好的语义、简单性和常识。
【Eolink 翻译】,Eolink Apikit = API 管理 + Mock + 自动化测试 + 异常监控 + 团队协作的一站式 API 生产平台,是一个跨平台(Windows、Mac、Linux、Browsers...)的 API 开发测试工具,支持 REST、Websocket、gRPC、TCP、UDP、SOAP等协议。
初创企业免费使用申请通道:https://easy-open-link.feishu.cn/share/base/form/shrcnpMe5dWtOkq2GoRWQ97oLlc