API风格指南
本风格指南推荐了API开发的最佳实践。
GraphQL 和 REST API
我们向客户提供了两种类型的API:
为了减少并行支持两种API的技术负担,它们应尽可能共享实现。例如,它们可以共享相同的服务。
前端
有关在前端开发时使用哪种API的详细信息,请参见前端指南。
实例变量
不要使用实例变量,没有必要(我们不像在Rails视图中那样需要访问它们),本地变量即可。
实体
始终使用一个Entity 来呈现端点的负载。
文档
每个新的或更新的API端点必须附带文档。文档应在同一合并请求中,或在严格必要的情况下,在与原始合并请求相同的里程碑的后续请求中。
有关以Markdown和OpenAPI定义文件形式记录API资源的详细信息,请参见文档风格指南 RESTful API页面。
方法和参数描述
每个方法都必须使用Grape DSL 进行描述(参见environments.rb 了解良好示例):
-
desc用于方法摘要。您应该传递给它一个块以添加其他详细信息,例如: -
添加该端点的GitLab版本。如果它被功能标志隐藏,则提及这一点:此功能由:feature_flag_symbol功能标志控制。
-
如果端点已弃用,以及如果是的话,其计划移除日期
-
params用于方法参数。这作为描述,参数验证和强制转换
良好的示例如下:
desc '获取所有广播消息' do
detail '此功能在GitLab 8.12中引入。'
success Entities::System::BroadcastMessage
end
params do
optional :page, type: Integer, desc: '当前页码'
optional :per_page, type: Integer, desc: '每页的消息数量'
end
get do
messages = System::BroadcastMessage.all
present paginate(messages), with: Entities::System::BroadcastMessage
end破坏性变更
我们不能对我们的REST API v4做出破坏性变更,即使在主要的GitLab版本中也是如此。参见什么是破坏性变更 和 什么不是破坏性变更。
我们的REST API维护自己的版本控制,独立于GitLab版本控制。当前的REST API版本是4。因为我们承诺遵循语义化版本控制,所以我们不能对其进行破坏性变更。目前没有计划或安排对我们的REST API进行主要版本更改(很可能是5)。
例外情况是标记为实验性或beta的API功能。这些功能可以随时删除或更改。
替代破坏性变更的做法
以下各节建议了替代破坏性变更的方法。
调整架构以适应变化而不破坏它
如果一个功能发生变化,我们应该旨在在不破坏API的情况下提供向后兼容性。
与其引入破坏性变更,不如更改API控制器层,以一种不会向API消费者展示任何变化的方式适应功能变更。
例如,我们将合并请求的_WIP_ 功能重命名为_Draft_。为了完成这一变更,我们:
-
在API响应中添加了一个新的
draft字段。 -
同时保留了旧的
work_in_progress字段。
客户没有遇到现有API集成的中断。
为功能移除维护API向后兼容性
即使一个端点所交互的功能在主要GitLab版本中被移除,我们也必须仍然维护API向后兼容性。
维护API向后兼容性的可接受解决方案包括:
-
从字段返回合理的静态值,或空响应(例如,
null或[])。 -
通过继续接受该参数但使其不再起作用,将该参数变为无操作。
关键原则是现有客户API集成不能出现错误。
端点继续以相同的字段响应并接受相同的参数,尽管底层功能交互已不再运行。
预期的变更必须提前记录在v4弃用指南中(documentation/restful_api_styleguide.md#deprecations)。
例如,当我们移除一个应用设置时,我们保留了旧的API字段(https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83984),它现在返回一个合理的静态值。
什么是破坏性变更
一些破坏性变更的示例包括:
- 移除或重命名字段、参数或枚举值。在JSON响应中,字段是任何JSON键。
- 移除端点。
- 添加新的重定向(并非所有客户端都遵循重定向)。
- 更改任何响应的内容类型。
- 更改响应中字段的类型。在JSON响应中,这将是任何
Number、String、Boolean、Array或Object类型更改为其他类型。 - 添加新的必需参数。
- 更改身份验证、授权或其他标头要求。
- 更改任何状态码,除了
500。
什么不是破坏性变更
一些非破坏性变更的示例包括:
- 任何添加性变更,如添加端点、非必需参数、字段或枚举值。
- 更改错误消息。
- 从
500状态码变更为任何支持的状态码(这是修复bug)。 - 更改响应中返回的字段顺序。
实验性、测试版和正式可用功能
你可以将API元素作为实验性和测试版功能添加。它们必须是添加性变更,否则会被归类为破坏性变更。
标记为实验或测试版的API元素不受破坏性变更策略约束,可以在未经事先通知的情况下随时更改或删除。
当处于实验状态时:
- 使用默认关闭的功能标志off by default。
- 当标志关闭时:
当处于测试版状态时:
- 使用默认开启的功能标志on by default。
- API文档必须记录测试版状态,并且功能标志必须被记录。
- OpenAPI文档不得描述这些变更。
当功能变为正式可用时:
声明式参数
Grape允许你仅访问由你的params块声明的参数。它会过滤掉传递但未被允许的参数。更多细节,请参阅Ruby Grape的declared()文档。
排除父命名空间的参数
默认情况下,declared(params)包含在所有父命名空间中定义的参数。更多细节,请参阅Ruby Grape的include_parent_namespaces文档。
在大多数情况下,你应该排除父命名空间的参数:
declared(params, include_parent_namespaces: false)何时使用declared(params)
当你将参数哈希作为参数传递给方法调用时,应始终使用declared(params)。
例如:
# 不好的写法
User.create(params) # 假设用户提交了`admin=1`... :)
# 好的写法
User.create(declared(params, include_parent_namespaces: false).to_h)declared(params)返回一个Hashie::Mash对象,你必须调用.to_h。
但我们可以在访问单个元素时直接使用params[key]。
For instance:
# good
Model.create(foo: params[:foo])数组类型
在Grape v1.3+中,数组类型必须使用coerce_with块定义,否则当从API请求传递字符串时参数会验证失败。详见Grape升级文档。
nil输入的自动转换
在Grape v1.3.3之前,值为nil的数组参数会被自动转换为空数组。然而,由于v1.3.3中的这个拉取请求,情况不再如此。例如,假设你定义了一个包含可选参数的PUT /test请求:
optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: '该规则的用户ID'通常,对PUT /test?user_ids的请求会导致Grape传递params为{ user_ids: nil }。
这可能会在与预期空白数组的端点引入错误,且未正确处理nil输入。为了保留之前的行为,有一个辅助方法coerce_nil_params_to_array!用于所有API调用的before块:
before do
coerce_nil_params_to_array!
end通过此更改,对PUT /test?user_ids的请求会导致Grape传递params为{ user_ids: [] }。
Grape跟踪器中有一个开放问题以简化此操作。
使用HTTP状态助手
对于非200的HTTP响应,使用lib/api/helpers.rb中提供的助手以确保正确行为(如not_found!或no_content!)。这些助手会在Grape内部throw并终止端点的执行。
对于DELETE请求,通常也应使用destroy_conditionally!助手,默认情况下成功返回204 No Content响应,若给定的If-Unmodified-Since头超出范围则返回412 Precondition Failed响应。该助手会对传入的资源调用#destroy,但也可以通过传递块实现自定义删除方法。
选择HTTP动词
区分PATCH和PUT
在Rails应用中,PATCH和PUT请求方法都会被路由到控制器的update方法。在使用Grape编写GitLab API的框架中,你必须为执行更新的端点显式设置PATCH或PUT HTTP动词。
如果端点更新给定资源的所有属性,使用PUT请求方法;如果端点更新给定资源的一些属性,使用PATCH请求方法。
以下是PATCH的好例子:PATCH /projects/:id/protected_branches/:name
以下是PUT的好例子:PUT /projects/:id/merge_requests/:merge_request_iid/approve
通常,一个好的PUT端点只有ids和一个动词(如上例中的“approve”),或者它们只有一个值并表示键值对。
Rails博客详细解释了为什么PATCH通常是执行更新的Web API端点的最合适动词。
在GitLab Rails代码库中使用API路径助手
因为我们支持在相对URL下安装GitLab,在使用Grape生成的API路径助手时必须考虑这一点。任何此类API路径助手的使用都必须包装在expose_path助手调用中。
例如:
- endpoint = expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid))自定义验证器
为了验证API请求中的某些参数,我们在进一步发送(如Gitaly)前对其进行验证。以下是我们目前已添加的自定义验证器及其用法。我们还撰写了一份关于如何添加新自定义验证器的指南。
使用自定义验证器
FilePath:
GitLab 支持多种功能,在这些功能中我们需要遍历文件路径。
FilePath 验证器
会针对不同情况验证参数值。主要检查路径是否为相对路径,以及是否包含使用 File::Separator 的 ../../ 相对遍历,还检查路径是否为绝对路径(例如 /etc/passwd/)。默认情况下,绝对路径是不被允许的。但是,你可以选择性地传入一个允许列表来指定允许的绝对路径,方式如下:
requires :file_path, type: String, file_path: { allowlist: ['/foo/bar/', '/home/foo/', '/app/home'] }
Git SHA:
Git SHA 验证器
检查 Git SHA 参数是否为有效的 SHA。它通过使用 commit.rb 文件中提到的正则表达式进行检查。
Absence:
Absence 验证器
检查给定参数哈希中的特定参数是否存在。
IntegerNoneAny:
IntegerNoneAny 验证器
检查给定参数的值是否为 Integer、None 或 Any 之一。它只允许上述提及的值之一继续请求流程。
ArrayNoneAny:
ArrayNoneAny 验证器
检查给定参数的值是否为 Array、None 或 Any 之一。它只允许上述提及的值之一继续请求流程。
EmailOrEmailList:
EmailOrEmailList 验证器
检查字符串或字符串列表的值是否仅包含有效的电子邮件地址。它只允许所有邮件地址都有效的列表继续请求流程。
添加新的自定义验证器
自定义验证器是在将参数发送到平台进行进一步处理之前验证参数的好方法。如果在最开始就识别出无效参数,它可以减少服务器与平台之间的来回通信。
如果你需要添加自定义验证器,它会添加到
位于 validators 目录下的独立文件中。
由于我们使用 Grape 来添加 API,
我们在验证器类中继承自 Grape::Validations::Validators::Base 类。
现在,你只需要定义 validate_param! 方法,该方法接受两个参数:params 哈希和要验证的 param 名称。
该方法的主体负责验证参数值的工作,并向调用方方法返回适当的错误消息。
最后,我们使用以下行注册验证器:
Grape::Validations.register_validator(<validator name as symbol>, ::API::Helpers::CustomValidators::<YourCustomValidatorClassName>)添加验证器后,请确保将其 rspec 测试添加到
位于 validators 目录下的独立文件中。
内部 API
内部 API 文档供内部使用。请保持其最新状态,以便我们知道不同组件使用了哪些端点。
避免 N+1 问题
为了避免在 API 端点返回记录集合时常见的 N+1 问题,我们应该使用预加载(eager loading)。
在 API 中执行此操作的标准方法是让模型实现名为 with_api_entity_associations 的作用域,以预加载 API 返回的关联和数据。此作用域的示例如下所示:
Issue 模型。
在同一个模型有多个 API 实体的情况下(例如 UserBasic、User 和 UserPublic),你应该谨慎地应用此作用域。你可能需要优化最基本的实体,后续实体在此基础上构建作用域。
with_api_entity_associations 作用域还会
自动预加载数据
用于 Todo targets 当它们在 待办事项 API 中返回时。
关于预加载的更多背景信息和讨论,请参阅此合并请求,该请求引入了相关范围。
通过测试验证
当API端点返回集合时,始终添加一个测试来验证该API端点当前及未来都不会存在N+1问题。我们可以使用ActiveRecord::QueryRecorder来实现这一点。
示例:
def make_api_request
get api('/foo', personal_access_token: pat)
end
it '避免N+1查询', :request_store do
# 首先,记录当返回单个记录时端点会执行多少个PostgreSQL查询
create_record
control = ActiveRecord::QueryRecorder.new { make_api_request }
# 现在创建第二个记录,并确保API不会比之前执行更多的查询
create_record
expect { make_api_request }.not_to exceed_query_limit(control)
end测试
为新API端点编写测试时,考虑使用位于/spec/fixtures/api/schemas的模式fixture。你可以期望响应与给定模式匹配:
expect(response).to match_response_schema('merge_requests')另见测试中验证N+1性能的内容。
包含变更日志条目
所有面向客户端的更改必须包含变更日志条目。这不包括内部API。