
在 Rocket.Chat 中构建实验性 REST API 命名空间/api/experimental的不稳定契约、类型化路由设计与五步实现指南【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.Chat导读本文基于 Rocket.Chat 仓库内的 docs/experimental-api-endpoints-plan.md 展开系统讲解该团队如何在不违反语义化版本semver承诺的前提下让 REST 端点能够随任意版本发布、变更甚至无警告移除。文章完整继承原计划的核心契约、源码级设计论证与五步实现方案并结合 apps/meteor/server/api/api.ts、ApiClass.ts 与 rest-typings 包的实际落地代码向读者展示如何为任意团队交付一套通用、可选、独立于稳定 SDK 的类型化实验端点机制。读完本文你将掌握Rocket.Chat 的 API 实例API.v1/API.experimental/API.default如何被创建与挂载、x-experimental不稳定信号的运行时实现原理、以及如何新增一个属于自己的实验性端点并将其安全提升promotion到/v1。一、问题定义允许不稳定端点上线而不破坏 semver1.1 核心目标Rocket.Chat 希望 REST 端点能够带着不稳定状态发布到生产环境但允许它们在任意一次发布中被更改或移除而无需触发主版本号major-version提升。要做到这一点机制必须是通用的——任何团队、任何功能模块都能直接使用而不是为某一个功能单独打补丁。1.2 契约The contract原计划文档给出了如下一段必须被所有调用方知晓的契约文本Endpoints under/api/experimental/...are unstable. They may change shape or be removed in any release, without notice and without a deprecation cycle. No semver promise attaches to this namespace.翻译过来即是位于/api/experimental/...之下的端点不稳定它们可能在任意一次发布中改变形态或被移除不提前通知、不经过弃用周期此命名空间不附带任何 semver 承诺。1.3 命名空间本身就是契约这套机制最精妙的设计在于命名空间namespace本身就是契约。调用方只要命中了/api/experimental/*光凭 URL 就等同于自愿选择进入不稳定区——无需阅读额外的文档也无需修改客户端配置。与此同时/v1隐式承载的 semver 稳定性承诺保持原样、不受任何影响。这一点在 api.ts 的 API 对象字面量中体现得直白v1、experimental、default三个实例并存分别以version区分见下文源码证据。1.4 规则只有新类型化 API 允许出现在 experimental 路由上计划文档明确规定实验性端点只能通过.get()/.post()/.put()/.delete()注册并且必须携带 AJV 的body/query/response校验器已被废弃的.addRoute()禁止在实验性路由上使用——新的表面积surface area不应诞生在遗留注册路径上。需要特别指出的是这是一条文档化规则而非编译器强制的规则。原因在计划文档与源码中都有交代API.experimental本质上只是一个普通的APIClass并没有在类型层面剔除.addRoute().addRoute()在其可达的每一处重载上都已携带deprecated注解见 ApiClass.ts 中第 783–815 行对addRoute的 JSDocIDE 与 lint 工具会提示开发者远离它。之所以放弃从类型上隐藏addRoute是因为类型化方法.get()等返回this——一个被限制的表面积只会保持到第一次链式注册为止要堵上这个口子就必须复制每一个类型化方法的签名并拓宽负责模式匹配APIClass的路由提取类型成本收益不成正比。二、为什么这么设计来自当前代码库的关键发现计划文档在动手前先梳理了既有 REST 体系的关键事实这些发现直接决定了最终方案。结合仓库源码逐一印证如下。2.1createApi({ version })把 version 字符串变成 URL 路径段在 api.ts 中createApi是一个薄封装本质是new APIClass({ apiPath: , useDefaultAuth: false, ...options })。而APIClass的构造函数ApiClass.ts会这样拼装路径this.apiPath [properties.apiPath, properties.version].filter(Boolean).join(/).replaceAll(//, /); ... this.router new RocketChatAPIRouter(/${this.apiPath}.replace(/\/$/, ).replaceAll(//, /));version直接成为apiPath段再由apiPath参与router的根路径组装。因此只要以version: experimental新建一个实例它天然挂在/api/experimental/name下——路由器内部逻辑一行都不用改。唯一的前提是新路由器仍需在startRestAPI中被挂载即计划中的 Step 2。2.2 类型化路由方法不与Endpoints联合类型绑定类型化路由方法.get()/.post()/.put()/.delete()对TSubPathPattern extends string泛型化见 ApiClass.ts 中的APIClass.method()及其各动词包装器并不是被keyof Endpoints约束的。也就是说注册的路由会向外累积进实例的TOperations类型参数再由 api.ts 底部的ExtractApiClassEndpoints读回。这意味着一个独立的 experimental 实例天然与类型系统协同工作不需要额外的类型体操。2.3 稳定客户端类型面的隔离依赖Endpoints接口PathPattern、Method、Path以及类型化客户端全部从Endpoints接口见 packages/rest-typings/src/index.ts派生。因此只要把 experimental 路径排除在Endpoints之外稳定客户端表面积就能保持纯净并且迫使任何想调用实验端点的代码做出显式的 opt-in。2.4 复用既有的弃用响应头框架弃用框架已经在往响应里写x-deprecation-*头writeDeprecationHeader位于 apps/meteor/server/lib/deprecationWarningLogger.ts。experimental 的x-experimental/Warning信号将镜像这一模式风格统一、维护成本低。2.5 认证、权限、限流、CORS、AJV 校验与指标全部免费获得Auth、权限、限流、CORS、AJV 校验与 Prometheus 指标均来自createApi以及startRestAPI中的中间件链。experimental 端点只要挂在同一管线之后就自动享有与/v1完全一致的这些能力——这正是 Step 1 中限流 watcher 必须覆盖 experimental被视为强制的根因详见 Step 1。三、提交约束一步一提交保证可二分与可回滚原计划对落地过程施加了严格的工程纪律值得任何大型改动借鉴每一步实现 恰好一个提交。没有一步被拆成多个提交也没有一个提交横跨多个步骤。保持历史可二分bisectable每个阶段独立可评审、可回滚并让 PR review 与计划 1:1 对齐。每步的提交必须让仓库处于可编译、lint 干净的状态yarn lint --quiet通过——不完整的工作在提交前必须被 squash。提交信息主题要指名步骤例如feat(api): add experimental API instance (step 1)。若某一步暴露出计划之外的前置条件则把它并入该步的唯一提交而不是额外引入计划外提交。四、五步实现详解Step 1 — 添加experimentalAPI 实例改动文件apps/meteor/server/api/api.ts1. 在API对象字面量中添加实例放置于v1与default之间experimental: createApi({ version: experimental, useDefaultAuth: true }),2. 为API类型注解增加experimental条目使其获得类型APIClass/experimental。如前文所述从该类型中隐藏addRoute()的方案曾被考虑但被否决。3. 设置变更时的路由刷新是必须的对等条件required parity而非可选项。契约承诺 experimental 端点免费获得限流只有当刷新回调覆盖到它们时才成立。以下settings.watch(...)回调必须同步更新API.experimental设置项回调API_Enable_Rate_Limiter_Limit_Time_DefaultreloadRoutesToRefreshRateLimiter()API_Enable_Rate_Limiter_Limit_Calls_DefaultreloadRoutesToRefreshRateLimiter()Accounts_CustomFieldssetLimitedCustomFields()已知缺口Known gap限流 watcher 已实现对等但Accounts_CustomFieldswatcher 目前仍只更新API.v1。当前这是无害的——尚无 experimental 端点会返回用户对象——但在出现此类端点之前必须补齐。在仓库当前代码api.ts 第 43–111 行中该步骤已经落地API类型包含experimental: APIClass/experimental实例位于v1与default之间且reloadRoutesToRefreshRateLimiter已经同时刷新API.v1与API.experimental两个实例的限流规则。验收标准AcceptanceAPI.experimental.get(ping, { ... }, handler)可编译并在GET /api/experimental/ping上提供服务。提交共 5 个中的第 1 个feat(api): add experimental API instanceStep 2 — 在请求管线中挂载 experimental 路由器改动文件apps/meteor/server/api/api.ts 中的startRestAPI1. 把.use(API.experimental.router)插入中间件链位置在.use(API.default.router)之前。顺序至关重要default是兜底catch-all路由器。2. 新增一个指向API.experimental的metricsMiddleware块让 experimental 流量被度量。指标是后续判断端点是否够格提升到/v1的**金丝雀canary**依据。因为所有块共享同一个/api挂载点每一块都需要自己的守卫否则一个请求会被采样多次带版本号的块通过basePathRegexopt-in只采样自己前缀下的路径兜底块负责API.default/api/info、/api/docs/json以及未匹配任何版本的/api/*通过excludePathRegexopt-out掉自己不属于的版本前缀。若缺少这个兜底块守卫会静默丢弃过去一直被采样的 default 路由流量。在仓库当前代码中startRestAPIapi.ts 第 113–162 行可以看到三块并排的metricsMiddleware.use(metricsMiddleware({ basePathRegex: new RegExp(/^\/api\/v1\//), api: API.v1, ... })) .use(metricsMiddleware({ basePathRegex: new RegExp(/^\/api\/experimental\//), api: API.experimental, ... })) .use( metricsMiddleware({ excludePathRegex: new RegExp(/^\/api\/(v1|experimental|apps)\//), api: { version: default }, ... }), )注意实际实现中excludePathRegex额外包含了apps前缀Apps Engine 的/api/apps并且 default 兜底块的version被显式标为default避免标签为空。metricsMiddleware的守卫逻辑可以在 apps/meteor/server/api/v1/middlewares/metrics.ts 中看到先按basePathRegex放行、再按excludePathRegex跳过采样完成后用api.version写入 Prometheus 标签。验收标准experimental 请求出现在 REST API Prometheus 指标中且标签必须是versionexperimental这个具体值而不是某个可区分的值。如果只是调整路径正则、却仍让 experimental 流量落入v1版本标签则不算达标。/api/v1/*与 default 路由流量各自仍应在其标签下被精确采样恰好一次。提交feat(api): mount experimental router and metricsStep 3 — 运行时unstable信号镜像弃用头新建文件apps/meteor/server/api/v1/middlewares/experimental.ts与既有中间件同目录存放。1. 编写一个中间件在来自 experimental 实例的每个响应上设置如下头Warning: 299 - experimental: endpoint is unstable and may change without notice x-experimental: true两者定位不同务必区分x-experimental: true是被支持的编程式信号supported programmatic signal——客户端应通过它来识别 experimental 响应Warning: 299仅是遗留兼容信号warn code 299 来自 RFC 7234该 RFC 连同Warning头本身已被 RFC 9111 废弃现代客户端预期既不生成也不解释它。它仅为那些仍然展示该头的工具而保留未来直接去掉也不算破坏性变更。头部写入风格应参考writeDeprecationHeaderapps/meteor/server/lib/deprecationWarningLogger.ts。仓库中该中间件已经实现见 experimental.ts其注释精确记录了上述设计取舍const WARNING_HEADER 299 - experimental: endpoint is unstable and may change without notice; export const experimentalWarningMiddleware ({ basePathRegex }: { basePathRegex: RegExp }): MiddlewareHandler async (c, next) { if (!basePathRegex.test(c.req.path)) { return next(); } c.res.headers.set(x-experimental, true); c.res.headers.set(Warning, WARNING_HEADER); await next(); };2. 把中间件注册在共享的/api挂载点上、cors之前通过basePathRegex限定到/api/experimental与 metrics 中间件守卫形状一致。它不能挂在API.experimental.router上因为cors在拒绝预检请求时会直接以 403/405 应答而不调用next()挂在路由器上的中间件永远不会覆盖那些响应。当前 api.ts 第 155 行的注册方式为.use(experimentalWarningMiddleware({ basePathRegex: new RegExp(/^\/api\/experimental(\/|$)/) })) .use(cors(settings))中间件把响应头预先设置在c.res.headers上下游 handler 无论最终产出什么响应包括 404 与 CORS 拒绝Hono 都会合并这些头——这正是连 404 和预检拒绝都带信号头的实现细节。验收标准每个/api/experimental/*响应都携带这两个头——包括 404 与 CORS 预检拒绝而/api/v1/*响应不携带。提交feat(api): add experimental unstable-signal middlewareStep 4 — 独立、可选的 SDK 类型改动文件packages/rest-typings/src/index.ts 新建声明文件1. 新建packages/rest-typings/src/experimental/index.ts新目录声明实验类型。命名风格参照既有 per-resource 端点类型如 packages/rest-typings/src/v1/channels/channels.tsexport type ExperimentalEndpoints { /experimental/name: { GET: (params: ...) ...; }; // ... };2. 从包根导出ExperimentalEndpoints但绝不把它并入interface Endpoints extends ...联合。这样PathPattern、Method、Path以及稳定类型化客户端就始终与 experimental 路径绝缘。当前 rest-typings/src/index.ts 第 275–277 行的导出方式即为此意// Opt-in experimental endpoint typings. Deliberately NOT part of the Endpoints // union above — see ./experimental for the rationale. export type * from ./experimental;而 packages/rest-typings/src/experimental/index.ts 顶部的注释亦声明这些类型有意不并入包根导出的Endpoints联合使稳定类型化客户端表面积不沾染不稳定路径需要类型化 experimental 调用的消费方必须显式导入ExperimentalEndpoints且每个路径键必须以/experimental/开头。3. 想要获得实验端点类型安全的消费者显式导入ExperimentalEndpoints即可。验收标准import type { Endpoints } from rocket.chat/rest-typings不包含 experimental 路径import type { ExperimentalEndpoints }包含。提交feat(rest-typings): add opt-in ExperimentalEndpointsStep 5 — 护栏因为这是一套通用机制1. 任何路径不得同时存在于两个联合类型中。类型层面的 CI 守卫曾被考虑但被否决联合类型的键是完整路径/experimental/x与/v1/x永远不会碰撞下面描述的过渡窗口也不会产生碰撞。因此该规则保留在文档中即可——提升promotion意味着把声明移动到稳定的*Endpoints类型中而不是在旧位置留一份副本。2. 提升路径Promotion path文档中需要写明——把一个端点稳定化等于把它复制到/v1可选地在过渡窗口期保留 experimental 路径的转发。移除则无需弃用周期但出于礼节应记录移除日志。3. Docs/CONTRIBUTING 说明在文档中陈述 no-semver 保证以及如何新增一个 experimental 端点让机制可被发现。仓库中与计划配套的 docs/experimental-api-endpoints.md 即承担了这一职责。4. OpenAPI/文档生成决策需审慎决定生成的 API 文档是只扫描Endpointsexperimental 端点被隐藏——通常是期望的还是也扫描ExperimentalEndpoints。提交chore(api): add experimental guardrails and docs五、测试清单计划文档给出了一份可用于验收整个机制的测试核对清单共 5 项GET /api/experimental/name可解析并返回x-experimentalWarning响应头/api/v1/*的响应保持不变不带 experimental 头experimental 路由上的 Auth / 权限 / 限流与/v1完全一致地生效Endpoints类型不包含 experimental 路径ExperimentalEndpoints包含experimental 请求出现在 REST API 指标中六、涉及文件一览文件变更apps/meteor/server/api/api.ts新增experimental实例、类型条目在startRestAPI中挂载新增 metrics 块apps/meteor/server/api/v1/middlewares/experimental.ts新x-experimental/Warning响应头中间件apps/meteor/server/api/v1/middlewares/metrics.tsbasePathRegex/excludePathRegex采样守卫packages/rest-typings/src/experimental/index.ts新ExperimentalEndpoints类型不并入Endpointspackages/rest-typings/src/index.ts导出ExperimentalEndpointsdocs / CONTRIBUTING记录契约与提升路径七、仓库当前落地状态与真实示例端点这份计划在 Rocket.Chat 当前代码库中已经基本落地是理解计划→实现映射的最佳案例。除了上文各步骤对应的代码外仓库已有一个真实的 experimental 端点可供对照——apps/meteor/server/api/experimental/rooms.setCategory.ts。该端点完整演示了计划的每一条规则使用API.experimental.post(rooms.setCategory, ...)注册而非.addRoute()携带 AJV 编译的body校验器isRoomsSetCategoryParamsPOST对roomIds: string[]minItems: 1、uniqueItems: true与可空的category通过not: { enum: [...SIDEBAR_SYSTEM_GROUP_KEYS] }排除系统分组做精确约束声明response的 200 / 400 / 401 / 403 多状态码校验复用了validateBadRequestErrorResponse等标准错误响应校验器开启authRequired: true并挂载 license 要求experimental-enterprise-features证明认证与授权在 experimental 路由上原样生效handler 内部通过this.bodyParams/this.userId访问上下文并使用API.experimental.success()/API.experimental.failure()返回结果——与/v1的APIClass辅助方法完全同源。与其配套的ExperimentalEndpoints声明位于 packages/rest-typings/src/experimental/index.ts其中定义了/experimental/rooms.setCategory的 POST 签名(params: { roomIds: string[]; category: string | null }) { success: true }与实现一一对应。这套机制的价值在于新功能的 API 可以在不影响任何稳定消费者的情况下尽早暴露给真实用户与内部团队试用用生产流量和指标versionexperimental标签作为是否足够成熟、可提升至/v1的客观依据——既保持了 Rocket.Chat/v1对外部生态的 semver 承诺又避免了新功能在发布前长期憋在内部、缺少真实反馈的窘境。【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.Chat创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考