关于REST API设计的文章。《星际战士》游戏中暴露API的部分
API是开发人员的用户界面——所以要努力让它变得令人愉快
使用RESTful url和操作
在任何地方都使用SSL,没有异常
一个API的好坏取决于它的文档——所以要有优秀的文档
版本通过URL,而不是通过头
使用查询参数进行高级过滤、排序和搜索
提供一种方法来限制从API返回哪些字段
从POST, PATCH和PUT请求返回一些有用的东西
HATEOAS目前还不实用
尽可能使用JSON,必要时只使用XML
你应该在JSON中使用camelCase,但是snake_case要容易阅读20%
默认为漂亮的打印&确保支持gzip
默认情况下不要使用回复信封
考虑将JSON用于POST、PUT和PATCH请求体
使用链接标头进行分页
提供一种自动加载相关资源表示的方法
提供一种重写HTTP方法的方法
为速率限制提供有用的响应标头
使用基于令牌的身份验证,在需要委托的地方通过OAuth2传输
包括响应头以方便缓存
定义一个可消耗的错误有效负载
有效地使用HTTP状态码
API的关键需求
为了帮助指导决策过程,我写下了API必须达到的一些要求:
- 它应该在有意义的地方使用网络标准
- 它应该对开发人员友好,并且可以通过浏览器地址栏进行探索
- 它应该简单,直观和一致,使采用不仅容易,而且令人愉快
- 它应该为大多数Enchant UI提供足够的灵活性
- 它应该是高效的,同时保持与其他需求的平衡
API是开发人员的UI——就像任何UI一样,确保用户的体验是经过仔细考虑的很重要!
使用RESTful url和操作
REST的关键原则包括将API分离为逻辑资源。使用HTTP请求操作这些资源,其中方法(GET、POST、PUT、PATCH、DELETE)具有特定的含义。
但是我能做什么资源呢?从API使用者的角度来看,这些应该是有意义的名词,而不是动词。Enchant的一些名词是ticket, user和customer。
注意:尽管您的内部模型可以整齐地映射到资源,但这并不一定是一对一的映射。这里的关键是不要将无关的实现细节泄露给API!你的API资源需要从API使用者的角度来看是有意义的。
一旦定义了资源,就需要确定哪些操作应用于它们,以及这些操作如何映射到API。RESTful原则提供了使用映射如下的HTTP方法处理CRUD操作的策略:
- GET /tickets—检索一个票的列表
- GET /tickets/12 -检索一个特定的票
- POST /tickets -创建一个新票
- PUT /tickets/12 -更新票号12
- PATCH /tickets/12 -部分更新12号票
- DELETE /tickets/12 -删除票号12
REST的优点是利用现有的HTTP方法在单个/tickets端点上实现重要的功能。没有方法命名约定,URL结构干净清晰。其他增值!
端点名称应该是单数还是复数?保持简单的原则也适用于此。实用的答案是保持URL格式一致并始终使用复数。不必处理奇复数(person/people, goose/geese)使API使用者的生活变得更好
但是你如何处理关系呢?如果一个关系只能存在于另一个资源中,RESTful原则提供了有用的指导。让我们看一个例子。《Enchant》中的一张票由许多消息组成。这些消息可以逻辑上映射到/tickets端点,如下所示:
GET /tickets/12/messages -检索票号12的消息列表
GET /tickets/12/messages/5 -检索票号12的消息#5
POST /tickets/12/messages -在ticket #12中创建一个新消息
PUT /tickets/12/messages/5 -更新票号12的消息#5
PATCH /tickets/12/messages/5 -部分更新了票号12的消息#5
DELETE /tickets/12/messages/5 -删除票号12的消息#5
替代方案1:如果关系可以独立于资源而存在,那么在资源的输出表示中只包含它的标识符是有意义的。然后,API使用者必须到达关系的端点。
替代方案2:如果一个独立存在的关系通常与资源一起被请求,那么API可以提供自动嵌入关系表示的功能,并避免对API的第二次攻击。干净的API和一个命中服务器。
不适合CRUD操作的操作怎么办?
这就是事情可能变得模糊的地方。有很多方法:
重新构造操作,使其看起来像资源的字段。如果操作不带参数,这是有效的。例如,一个激活操作可以映射到一个布尔激活字段,并通过补丁更新到资源。
将其视为具有RESTful原则的子资源。例如,GitHub的API允许你用PUT /gist /:id/星号标记gist,用DELETE /gist /:id/星号标记非星号。
有时确实没有办法将操作映射到合理的RESTful结构。例如,将多资源搜索应用于特定资源的端点实际上没有意义。在这种情况下,/search最有意义,即使它不是资源。这是可以的-只要从API使用者的角度做正确的事情,并确保清楚地记录以避免混淆。
https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api#requirements
在任何地方都使用SSL,没有异常
始终使用SSL。没有例外。今天,您的web api可以从任何有互联网的地方访问(如图书馆、咖啡店、机场等)。并非所有这些都是安全的。许多人根本不加密通信,如果身份验证凭证被劫持,就很容易被窃听或模仿。
始终使用SSL的另一个优点是,有保证的加密通信简化了身份验证工作——您可以使用简单的访问令牌,而不必为每个API请求签名。
要注意的一件事是对API url的非ssl访问。不要将它们重定向到对应的SSL。而是抛出一个严重的错误!当自动重定向到位时,配置不良的客户端可能会在未加密的端点上不知不觉地泄漏请求参数。硬错误可确保及早捕获此错误并正确配置客户端。
文档
API的好坏取决于它的文档。文档应该很容易找到,并且公众可以访问。大多数开发人员在尝试任何集成工作之前都会检查文档。当文档隐藏在PDF文件中或需要登录时,它们不仅很难找到,而且也不容易搜索。
文档应该展示完整的请求/响应周期示例。请求最好是可粘贴的示例——可以粘贴到浏览器的链接或可以粘贴到终端的curl示例。GitHub和Stripe在这方面做得很好。
一旦你发布了一个公共API,你已经承诺不会在没有通知的情况下破坏它。文档必须包括任何弃用计划和外部可见的API更新的细节。更新应该通过博客(即更新日志)或邮件列表(最好两者都有!)
版本
一定要对API进行版本化。版本控制可以帮助您更快地迭代,并防止无效请求击中已更新的端点。它还有助于平滑任何主要API版本的转换,因为您可以在一段时间内继续提供旧的API版本。
关于API版本应该包含在URL中还是包含在头文件中,人们有不同的看法。从学术角度来说,它应该在标题中。但是,版本需要在URL中,以确保跨版本资源的浏览器可探索性(还记得本文顶部指定的API需求吗?),并为开发人员提供更简单的体验。
我是Stripe对API版本控制方法的忠实粉丝——URL有一个主版本号(v1),但API有基于日期的子版本,可以使用自定义HTTP请求头来选择。在这种情况下,主版本提供了API的整体结构稳定性,而子版本则负责较小的更改(字段弃用、端点更改等)。
API永远不会完全稳定。改变是不可避免的。重要的是如何管理这种变化。对许多api来说,良好记录和公布的数月弃用计划是一种可接受的实践。归根结底,考虑到API的行业和可能的消费者,什么是合理的。
结果筛选,排序和搜索
最好保持基本资源url尽可能精简。复杂的结果过滤器、排序需求和高级搜索(当限制为单一类型的资源时)都可以作为基本URL顶部的查询参数轻松实现。让我们更详细地看看这些:
过滤:为实现过滤的每个字段使用唯一的查询参数。例如,当从/tickets端点请求门票列表时,您可能希望将这些限制为仅处于打开状态的门票。这可以通过类似GET /tickets?state=open这样的请求来完成。这里,state是一个实现筛选器的查询参数。
排序:与筛选类似,可以使用泛型参数排序来描述排序规则。通过让排序参数接受一个由逗号分隔的字段列表,每个字段都可能带有一个一元负来表示降序排序,从而满足复杂的排序需求。让我们来看一些例子:
GET /tickets? sort=-priority - 按优先级降序检索票据列表
GET /tickets? sort=-priority,created_at - 按优先级降序检索票据列表。在特定的优先级范围内,较旧的门票将优先订购
搜索:有时基本的过滤器是不够的,你需要全文搜索的功能。也许你已经在使用ElasticSearch或其他基于Lucene的搜索技术。当全文搜索被用作检索特定类型资源的资源实例的机制时,它可以作为资源端点上的查询参数在API上公开。假设q .搜索查询应该直接传递给搜索引擎,API输出应该与普通列表结果相同的格式。
将这些组合在一起,我们可以构建这样的查询:
GET /tickets?sort=-updated_at - 检索最近更新的机票
GET /tickets?state=closed&sort=-updated_at - 检索最近关闭的门票
GET /tickets?q=return&state=open&sort=-priority,created_at - 检索提到“return”一词的最高优先级的打开状态的门票
常见查询的别名
为了使API体验对普通消费者来说更愉快,可以考虑将一组条件打包到易于访问的RESTful路径中。例如,上面的最近关闭的门票查询可以打包为GET /tickets/recently_closed
限制API返回哪些字段
API使用者并不总是需要资源的完整表示。选择和选择返回字段的能力可以让API使用者最大限度地减少网络流量,并加快他们自己对API的使用。
使用字段查询参数,该参数接受一个以逗号分隔的字段列表。例如,下面的请求将检索足够的信息来显示已排序的未开票列表:
GET /tickets?fields=id,subject,updated_at&state=open&sort=-updated_at
注意:此方法也可以与相关资源的自动加载相结合:
GET /tickets?embed=customer&fields=id,customer.id,customer.name
更新和创建应该返回一个资源表现物
PUT, POST或PATCH调用可以对底层资源的字段进行修改,这些字段不是提供的参数的一部分(例如:created_at或updated_at时间戳)。为了防止API使用者为了更新的表示而不得不再次访问API,可以让API返回更新的(或创建的)资源作为响应的一部分。
在POST导致创建的情况下,使用HTTP 201状态码并包括指向新资源URL的Location头。除了包含新创建的资源表示形式作为响应主体外,还应该包含这两个。
HATEOAS目前还不实用
对于API使用者是否应该创建链接,或者是否应该将链接提供给API,有很多不同的意见。rest式设计原则指定了HATEOAS,它大致规定了与端点的交互应该在输出表示附带的元数据中定义,而不是基于带外信息。
虽然网络通常是基于HATEOAS类型的原则(我们进入一个网站的首页,并根据我们在页面上看到的链接进行链接),但我认为我们还没有准备好在api上使用HATEOAS。在浏览网站时,将点击哪些链接是在运行时做出的决定。然而,对于API,关于发送什么请求的决定是在编写API集成代码时做出的,而不是在运行时做出的。决策是否可以延迟到运行时?当然,沿着这条路走下去也没有什么好处,因为代码仍然不能在不破坏的情况下处理重大的API更改。也就是说,我认为HATEOAS很有前途,但还没有准备好进入黄金时段。必须投入更多的努力来定义标准和围绕这些原则的工具,以充分实现其潜力。
现在,最好假设用户可以访问文档,并在输出表示中包含资源标识符,API消费者在制作链接时将使用这些标识符。坚持使用标识符有几个好处——在网络上流动的数据被最小化,API使用者存储的数据也被最小化(因为它们存储的是小标识符,而不是包含标识符的url)。
此外,鉴于这篇文章提倡在URL中存储版本号,从长远来看,API使用者存储资源标识符比存储URL更有意义。毕竟,标识符在不同版本之间是稳定的,但表示它的URL不是!
尽可能使用JSON,必要时只使用XML
XML并不是API的最佳选择。它很冗长,很难解析,也很难阅读,它的数据模型与大多数编程语言的数据模型不兼容,当输出表示的主要需求是从内部表示序列化时,它的可扩展性优势就不重要了。我还可以继续……
我不打算花太多精力解释这个。需要注意的关键是,今天您将很难找到任何仍然支持XML的主要API。你也不应该。
也就是说,如果您的客户基础由大量的企业客户组成,您可能会发现自己无论如何都必须支持XML。如果你一定要这么做,你会发现自己有一个新问题:
媒体类型应该根据Accept headers 更改还是根据URL更改?为了确保浏览器的可探索性,它应该在URL中。这里最明智的选择是将.json或.xml扩展名附加到端点URL。
字段名的snake_case vs camelCase
如果你使用JSON (JavaScript对象表示法)作为你的主要表示格式,“正确”的事情是遵循JavaScript命名约定——这意味着字段名的骆驼格!如果你选择用各种语言构建客户端库,最好使用它们的惯用命名约定——c#和Java使用camelCase, python和ruby使用snake_case。
精神食粮:我一直觉得snake_case比JavaScript约定的camelCase更容易阅读。我只是没有任何证据来支持我的直觉,直到现在。基于2010年对camelCase和snake_case的眼球追踪研究,snake_case比camelCase容易阅读20% !这种对可读性的影响将影响API的可探索性和文档中的示例。
许多流行的JSON api都使用snake_case。我怀疑这是由于服务器端序列化库遵循它们内置的底层语言的命名约定。也许我们需要让JSON序列化库处理命名约定转换。
默认为漂亮的打印&确保支持gzip
An API that provides white-space compressed output isn't very fun to look at from a browser. Although some sort of query parameter (like ?pretty=true) could be provided to enable pretty printing, an API that pretty prints by default is much more approachable. The cost of the extra data transfer is negligible, especially when you compare to the cost of not implementing gzip.
提供white-space输出的API从浏览器上看并不是很有趣。虽然可以提供一些查询参数(比如?pretty=true)来启用漂亮打印,但在默认情况下,漂亮打印的API要容易得多。额外数据传输的成本可以忽略不计,特别是与不实现gzip的成本相比。
考虑一些用例:如果API使用者正在调试,并让他们的代码打印出从API接收到的数据——在默认情况下,这些数据是可读的。或者,如果消费者获取了他们的代码生成的URL,并直接从浏览器中点击它——默认情况下,它将是可读的。这些都是小事。让API使用起来更愉快的小事情!
但是所有额外的数据传输怎么办?
让我们用一个真实的例子来看看这个问题。我从https://api.github.com/users/veesahni中提取了一些数据,该API默认使用漂亮的打印。我也会做一些gzip比较:
curl https://api.github.com/users/veesahni > with-whitespace.txt$
ruby -r json -e 'puts JSON.parse(STDIN.read)' < with-whitespace.txt > without-whitespace.txt gzip -c with-whitespace.txt > with-whitespace.txt.gz
gzip -c without-whitespace.txt > without-whitespace.txt.gz
输出文件的大小:
without-whitespace.txt - 1221 bytes
with-whitespace.txt - 1290 bytes
without-whitespace.txt.gz - 477 bytes
with-whitespace.txt.gz - 480 bytes
在本例中,当没有使用gzip时,空白使输出大小增加5.7%,当使用gzip时增加0.6%。另一方面,gzip本身提供了超过60%的带宽节省。由于漂亮打印的成本相对较小,最好在默认情况下进行漂亮打印,并确保支持gzip压缩!
不要默认使用信封,但在需要时可以使用
许多api将它们的响应包装在这样的信封中:
{
"data" : {
"id" : 123,
"name" : "John"
}
}
这样做有几个理由——它可以很容易地包含额外的元数据或分页信息,一些REST客户机不允许容易地访问HTTP headers& JSONP请求不能访问HTTP headers。然而,随着诸如CORS(https://fetch.spec.whatwg.org/)和RFC 5988中的Link头等标准的迅速采用,封装开始变得没有必要。
我们可以通过在默认情况下保持无信封并且只在特殊情况下进行信封来证明API。
在特殊情况下如何使用信封?
有两种情况确实需要信封——如果API需要支持JSONP上的跨域请求,或者如果客户端不能使用HTTP报头。
为了支持跨域JSONP:这些请求附带一个额外的查询参数(通常命名为callback或JSONP),表示回调函数的名称。如果该参数存在,API应该切换到全信封模式,在这种模式中,它总是响应一个200 HTTP状态码,并在JSON有效负载中传递真实的状态码。任何附加的HTTP报头都应该被映射到JSON字段,如下所示:
callback_function({
status_code: 200,
next_page: "https://..",
response: {
... actual JSON response body ...
}
})
要支持有限的HTTP客户端:允许一个特殊的查询参数?envelope=true,它将在没有JSONP回调函数的情况下触发封装。
JSON编码的POST, PUT和PATCH体
如果您遵循本文中的方法,那么您已经接受了所有API输出的JSON。让我们考虑JSON作为API输入。
许多API在其API请求体中使用URL编码。URL编码就像它听起来的那样——请求体中键值对的编码使用与URL查询参数中编码数据相同的约定。这很简单,得到广泛支持,可以完成工作。
但是,URL编码有一些问题。它没有数据类型的概念。这迫使API从字符串中解析整数和布尔值。此外,它没有真正的层次结构概念。尽管有一些约定可以从键值对中构建一些结构(比如将[]附加到键后以表示数组),但这与JSON的原生层次结构是无法相比的。
如果API很简单,我认为URL编码就足够了。但我认为这与输出格式不一致。
对于基于JSON的API,也应该坚持使用JSON进行API输入。
接受JSON编码的POST, PUT和PATCH请求的API还应该要求Content-Type报头设置为application/ JSON或抛出415不支持的媒体类型HTTP状态码。
分页
喜欢封装的api通常在封装本身中包含分页数据。我不怪他们——直到最近,也没有多少更好的选择。现在包含分页细节的正确方法是使用RFC 8288引入的Link头。
使用Link头的API可以返回一组现成的链接,这样API使用者就不必自己构造链接了。当分页是基于游标时,这一点尤其重要。下面是一个正确使用Link头的例子,摘自GitHub的文档:
Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next", <https://api.github.com/user/repos?page=50&per_page=100>; rel="last"
但这并不是一个完整的解决方案,因为许多api喜欢返回额外的分页信息,比如可用结果总数的计数。需要发送计数的API可以使用自定义HTTP报头,如X-Total-Count。
自动加载相关的资源表示
在许多情况下,API使用者需要加载与所请求的资源相关(或引用)的数据。如果允许根据需要返回相关数据并与原始资源一起加载,而不是要求消费者反复访问API以获取此信息,那么将显著提高效率。
然而,由于这违背了一些RESTful原则,我们可以通过仅基于embed(或expand)查询参数来最小化偏差。
在这种情况下,embed将是一个以逗号分隔的要嵌入的字段列表。点表示法可用于引用子字段。例如:
GET /tickets/12?embed=customer.name,assigned_user
这将返回一个包含额外细节的票,如:
{
"id" : 12,
"subject" : "I have a question!",
"summary" : "Hi, ....",
"customer" : {
"name" : "Bob"
},
assigned_user: {
"id" : 42,
"name" : "Jim",
}
}
当然,实现这类功能的能力实际上取决于内部复杂性。这种嵌入很容易导致N+1选择问题。
重写HTTP方法
一些HTTP客户端只能处理简单的GET和POST请求。为了增加对这些有限客户端的可访问性,API需要一种重写HTTP方法的方法。虽然这里没有任何硬标准,但流行的惯例是接受请求头X-HTTP-Method-Override,其中包含PUT、PATCH或DELETE之一的字符串值。
注意,重写头应该只在POST请求上被接受。GET请求不应该改变服务器上的数据!
速度限制
为了防止滥用,标准做法是向API添加某种速率限制。RFC 6585引入了 HTTP状态码429 Too Many Requests来适应这种情况。
但是,在实际达到极限之前通知消费者它们的极限是非常有用的。这是一个目前缺乏标准的领域,但有许多使用HTTP响应标头的流行约定。
至少要包含以下头文件:
X-Rate-Limit-Limit -当前时间段允许的请求数
X-Rate-Limit-Remaining -当前期间剩余的请求数
X-Rate-Limit-Reset -当前周期剩余的秒数
为什么X-Rate-Limit-Reset使用的是剩余秒数而不是时间戳?
时间戳包含各种有用但不必要的信息,比如日期,可能还有时区。API使用者实际上只想知道他们什么时候可以再次发送请求&用最少的额外处理来回答这个问题的秒数。它还避免了与时钟倾斜相关的问题。
一些api使用UNIX时间戳(从epoch开始的秒)进行X-Rate-Limit-Reset。不要这样做!
为什么对X-Rate-Limit-Reset使用UNIX时间戳是不好的做法?
HTTP规范已经指定使用RFC 1123日期格式(目前在date, If-Modified-Since & Last-Modified HTTP报头中使用)。如果要指定一个接受某种时间戳的新HTTP标头,它应该遵循RFC 1123约定,而不是使用UNIX时间戳。
身份验证
RESTful API应该是无状态的。这意味着请求身份验证不应该依赖于cookie或会话。相反,每个请求都应该带有某种排序身份验证凭据。
通过始终使用SSL,身份验证凭据可以简化为在HTTP基本身份验证的用户名字段中传递的随机生成的访问令牌。这样做的好处是它完全是浏览器可探索的——如果浏览器从服务器接收到401未授权状态码,它会弹出一个提示,要求提供凭据。
但是,这种基于基本身份验证的令牌方法仅适用于让用户将令牌从管理接口复制到API使用者环境的实际情况。在不可能实现这一点的情况下,OAuth 2应该用于向第三方提供安全的令牌传输。OAuth 2使用持名令牌,并依赖SSL进行底层传输加密。
需要支持JSONP的API将需要第三种身份验证方法,因为JSONP请求不能发送HTTP基本身份验证凭据或承载令牌。在这种情况下,可以使用一个特殊的查询参数access_token。注意:使用令牌查询参数存在固有的安全问题,因为大多数web服务器将查询参数存储在服务器日志中。
不管怎样,上面的三个方法都只是跨API边界传输令牌的方法。实际的底层令牌本身可能是相同的。
缓存
HTTP提供了一个内置的缓存框架!您所要做的就是包含一些额外的出站响应标头,并在收到一些入站请求标头时进行一些验证。
有两种方法:ETag和Last-Modified
ETag:在生成响应时,包含一个HTTP头ETag,其中包含表示的散列或校验和。当输出表示改变时,这个值应该改变。现在,如果入站HTTP请求包含一个带有匹配ETag值的if - none - match报头,API应该返回一个304 Not Modified状态码,而不是资源的输出表示。
Last-Modified:这基本上与ETag类似,除了它使用时间戳。响应头Last-Modified包含RFC 1123格式的时间戳,该时间戳根据If-Modified-Since进行验证。请注意,HTTP规范有3种可接受的日期格式,服务器应该准备好接受其中任何一种。
Errors
就像HTML错误页面向访问者显示有用的错误消息一样,API应该以已知可消费的格式提供有用的错误消息。错误的表示应该与任何资源的表示没有区别,只是有自己的一组字段。
API应该总是返回合理的HTTP状态代码。API错误通常分为两种类型:用于客户端问题的400系列状态代码和用于服务器问题的500系列状态代码。至少,API应该标准化所有400个系列错误都带有可消耗的JSON错误表示。如果可能(即如果负载均衡器和反向代理可以创建自定义错误体),这应该扩展到500系列状态码。
JSON错误体应该为开发人员提供一些东西——一条有用的错误消息,一个唯一的错误代码(可以在文档中找到更多细节),可能还有一个详细的描述。JSON输出表示如下所示:
{
"code" : 1234,
"message" : "Something bad happened :(",
"description" : "More details about the error here"
}
PUT、PATCH和POST请求的验证错误将需要字段分解。最好的建模方法是使用一个固定的顶级错误代码来验证失败,并在一个额外的错误字段中提供详细的错误,如下所示:
{
"code" : 1024,
"message" : "Validation Failed",
"errors" : [
{
"code" : 5432,
"field" : "first_name",
"message" : "First name cannot have fancy characters"
},
{
"code" : 5622,
"field" : "password",
"message" : "Password cannot be blank"
}
]
}
HTTP status codes
HTTP定义了一堆有意义的状态代码,这些代码可以从API返回。可以利用这些来帮助API使用者相应地路由其响应。我整理了一个简短的清单,你一定要使用:
200 OK -响应成功的GET, PUT, PATCH或DELETE操作。也可以用于不产生创建的POST。
201 Created -对创建POST的响应。应该与指向新资源位置的Location头相结合吗
204 No Content -响应一个成功的请求,但不会返回body(比如DELETE请求)
304未修改-当HTTP缓存头正在使用时使用
400 Bad Request——请求格式不正确,比如请求体没有解析
401未经授权-没有提供或无效的身份验证详细信息。如果API是从浏览器中使用,那么触发一个验证弹出框也很有用
403 Forbidden -认证成功,但认证用户不能访问资源
404 Not Found -当请求一个不存在的资源时
405方法不允许-当请求的HTTP方法不允许被验证的用户使用时
410 Gone -表示该端点的资源不再可用。作为旧API版本的通用响应非常有用
415不支持的媒体类型-如果在请求中提供了错误的内容类型
422不可处理实体-用于验证错误
429请求过多-由于速率限制,请求被拒绝
总结
API是面向开发人员的用户界面。努力确保它不仅是功能性的,而且使用起来也很愉快。