Backstage 插件 OpenAPI Schema-First 开发实战:从规范到类型化 Router 与自动生成 Client
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
导读
本文面向 Backstage 插件开发者,介绍如何基于 OpenAPI 规范以 Schema-First(规范优先)的方式驱动插件后端与前端客户端的开发。通过本教程,你将掌握在 Backstage 插件中存放与校验 OpenAPI 规范、从规范生成带强类型约束的 Express Router 与自动生成的 API Client、以及借助测试流量反向验证规范与实现一致性的完整工作流,让规范真正成为插件生命周期中的"单一事实来源"。
该方案由 Backstage OpenAPI 工具项目(OpenAPI tooling project area)提供,相关命令统一封装在@backstage/repo-tools包中,属于实验性(Experimental)能力。当前仓库中@backstage/backend-openapi-utils包即为运行时支撑实现,可用于对照学习。
这套工具能为你带来什么
目标非常明确:让 OpenAPI 规范与插件生命周期更紧密地耦合,具体提供三类能力:
- 类型化的 Express Router:根据规范生成带强类型约束的
expressRouter,在开发阶段为输入与输出值提供强力护栏(guardrails)。支持 query、path 参数和 request body 的完整类型推导,同时对 headers 与 cookies 提供实验性支持。 - 自动生成的客户端:基于规范生成与插件后端交互的客户端代码,覆盖所有请求类型、参数、body 与返回类型,并提供低层接口以便更高层的库在此基础上做定制。
- 校验与验证工具:确保 API 实现与规范始终保持同步,包括在单元测试阶段对请求/响应进行实体验证。
从源码结构看,这一系列命令集中在 packages/repo-tools/src/commands/package/schema/openapi(面向单个插件包)与 packages/repo-tools/src/commands/repo/schema/openapi(面向整个仓库)两个目录下,分别提供generate、validate、diff、fuzz、lint等子命令。
前置条件
技术要求
本教程假定你已经具备以下基础:
- 会构建一个 Backstage 插件;
- 熟悉
Express.js与TypeScript; - 了解 OpenAPI 3.1 Schema 规范。
OpenAPI 版本支持
Backstage 同时支持 OpenAPI 3.0 与 3.1 规范。如果已有 3.0 的存量规范,官方建议迁移到 3.1,可以使用oasdiff upgrade spec.yaml自动完成转换。主要变化包括:
- 将
nullable: true替换为type: ['string', 'null'],或改用anyOf/oneOf; - 从 path 参数中移除
allowReserved(在 3.1 中该属性仅对 query/cookie 参数有效)。
值得注意的是,packages/backend-openapi-utils/README.md 中声明该运行时包"只支持 OpenAPI 3.1 规范",因此将规范统一升级到 3.1 能获得最完整的工具链支持。
环境搭建
在工作区根目录安装@backstage/repo-tools。该包包含插件所需的全部 OpenAPI 相关命令,后续教程会反复使用:
yarn add --dev @backstage/repo-tools另外,若要使用破坏性变更检测(package schema openapi diff命令),还需要在系统上安装oasdiffCLI;同时请确保java可执行文件在 PATH 中(OpenAPI Generator 依赖 JVM 运行)。
存放你的 OpenAPI 规范
规范文件应当放在后端插件的src/schema目录下。例如给 catalog 插件添加规范时,需要在plugins/catalog-backend下新建src/schema目录,即plugins/catalog-backend/src/schema,并在其中放置openapi.yaml文件。
目前仅支持
.yaml扩展名,.yml不被支持。
这一约定在源码中有明确对应:packages/repo-tools/src/lib/openapi/constants.ts中定义了YAML_SCHEMA_PATH = 'src/schema/openapi.yaml',所有 OpenAPI 命令都会按此路径定位当前插件的规范文件。同时,该文件还定义了生成产物的输出路径OUTPUT_PATH = 'src/schema/openapi/generated',以及旧版单文件产物路径OLD_SCHEMA_PATH = 'src/schema/openapi.generated.ts'——如果你曾使用旧版生成方式,新版工具会自动检测并清理这个旧文件。
仓库内 docs/openapi/definitions/auth.yaml 提供了一份真实的 OpenAPI 3.0.1 示例规范(对应@backstage/auth-backend的 auth-provider API),其中展示了 query 参数、header 参数、cookie 参数、oneOf响应体以及 components/schemas 定义等写法,可作为编写规范的参考样例。
校验你的规范
编写完openapi.yaml后,可以在插件目录下运行以下命令,验证它是否是一份结构上合法的 OpenAPI 3.x 文档:
yarn backstage-repo-tools package schema openapi validate该命令会检查规范能否被正确解析并符合 OpenAPI 规范。建议在任何从规范生成代码的操作之前先执行它。
底层实现可参考 packages/repo-tools/src/commands/package/schema/openapi/validate.ts:它调用getPathToCurrentOpenApiSpec()定位当前插件的src/schema/openapi.yaml,再通过loadAndValidateOpenApiYaml()完成解析与校验,成功时输出绿色提示OpenAPI spec is valid.,失败时以非零退出码结束并打印错误详情。
规范风格与最佳实践检查
如果需要针对风格和最佳实践做 lint 检查,可额外运行(注意这里是仓库级repo命令):
yarn backstage-repo-tools repo schema openapi lint从规范生成类型化的 Express Router
在插件目录下运行:
yarn backstage-repo-tools package schema openapi generate --server该命令会在src/schema/openapi/generated目录下生成一个router.ts文件,其中包含内嵌的 OpenAPI 规范(以as const形式导出的spec常量)以及一个工厂函数createOpenApiRouter,用于创建与规范类型完全匹配的 Express Router。
建议把这条命令写入你的package.json以便复用;也可以将服务端与客户端生成合并成一条命令:
yarn backstage-repo-tools package schema openapi generate --server --client-package <clientPackageDirectory>在插件中接入生成的 Router
修改插件的router.ts或createRouter.ts,接入生成的 Router:
+ import { createOpenApiRouter } from '../schema/openapi'; - import Router from 'express-promise-router'; ... export async function createRouter( options: RouterOptions, ): Promise<express.Router> { + const router = await createOpenApiRouter(); - const router = Router();生成原理与源码视角
从 packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts 可以看到生成过程的完整链路:
- 读取
src/schema/openapi.yaml,通过@openapitools/openapi-generator-cli的typescript生成器配合仓库内模板 templates/typescript-backstage-server.yaml 生成基础类型与EndpointMap; - 生成
src/schema/openapi/generated/router.ts,其核心是调用@backstage/backend-openapi-utils包导出的createValidatedOpenApiRouterFromGeneratedEndpointMap<EndpointMap>(spec, options),其中spec以JSON.stringify(yaml, null, 2) as const形式内嵌; - 自动生成
src/schema/openapi/index.ts(内容为export * from './generated'),因此插件侧只需要import { createOpenApiRouter } from '../schema/openapi'即可; - 生成后会自动执行 lint 与 prettier 格式化,并按
packages/repo-tools/src/lib/openapi/constants.ts中的OPENAPI_IGNORE_FILES清理无用的模板文件。
@backstage/backend-openapi-utils正是类型化 Router 的运行时核心。根据其 README,该包基于oatx库改造而来,用于覆写 Express 的值类型。它提供开箱即用的createOpenApiRouter,也支持传入validatorOptions做定制;若需要在运行时动态修改规范,还可以直接使用createValidatedOpenApiRouter<typeof newSpec>(newSpec, validatorOptions)。
一个常见的坑是:当响应content中定义了 charset(例如response.content['application/json; charset=utf-8'])时,响应类型可能会被推导为unknown,应避免在 content 键中携带 charset。
从规范生成类型化的客户端
在当前后端插件目录下运行:
yarn backstage-repo-tools package schema openapi generate --client-package <plugin-client-directory>其中<plugin-client-directory>是一个需要你新建的目录和 npm 包。通用做法是给插件的 common 包新增一个入口,即plugins/<plugin-name>-common/client。同样建议把该命令加入package.json以便复用。
生成的客户端会在<plugin-client-directory>/src/schema/openapi/generated目录下产出DefaultApiClient类以及全部生成类型。
客户端生成的前提条件
根据 docs/openapi/generate-client.md,生成客户端前需要满足两点:
- 将 OpenAPI 文件的
info.title设置为你的 pluginId,例如:
info: # your pluginId title: catalog- 找到或新建一个用于承载生成客户端代码的插件包。目前工具不支持生成一个全新插件,只会生成客户端文件。
在现有 Client 中封装使用
以CatalogClient为例,将生成的DefaultApiClient作为内部实现细节封装起来:
+ import { DefaultApiClient } from '../schema/openapi/generated'; export class CatalogClient implements CatalogApi { + private readonly apiClient: DefaultApiClient; constructor(options: { discoveryApi: { getBaseUrl(pluginId: string): Promise<string> }; fetchApi?: { fetch: typeof fetch }; }) { + this.apiClient = new DefaultApiClient(options); } ...具体如何使用这些类型,取决于你的类型命名(与规范中的 schema 名称一一对应)。
生成的DefaultApiClient可以直接用于日常 API 调用;如果需要更强的定制能力,可以在其外层封装一个包装类来调整客户端的"口味"(例如统一错误处理、日志、认证头注入等)。
生成客户端注意点
根据 docs/openapi/generate-client.md,不要从src/schema/openapi/generated父目录的子文件夹中导入任何内容,所有需要的东西都应从src/schema/openapi/generated/index.ts导出,主要包括:
DefaultApiClient——用于访问你的具体规范对应 API 的客户端;- 各种请求/响应类型——名称与规范中的定义保持一致,可从 index 直接导入。
从源码看,packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts 会使用typescript-backstage-client.yaml模板生成代码,随后自动写入父目录index.ts(内容为export * from './generated'),并执行 lint、prettier 与 import 去重,最后清理.openapi-generator-ignore、.gitattributes等临时产物。
用测试流量验证规范与实现的一致性
在插件的createRouter.test.ts或router.test.ts中加入以下改动:
+ import { wrapServer } from '@backstage/backend-openapi-utils/testUtils'; + import type { Server } from 'node:http'; ... describe('createRouter', () => { - let app: express.Express; + let app: Server; ... - app = express().use(router); + app = await wrapServer(express().use(router));wrapServer会建立一个代理,在测试期间捕获所有请求与响应,并对照你的 OpenAPI 规范进行校验。任何规范与实际 API 行为之间的不匹配都会以测试失败的形式报告出来。
完整示例
下面是 docs/openapi/test-case-validation.md 提供的完整测试示例:
import { wrapServer } from '@backstage/backend-openapi-utils/testUtils'; import express from 'express'; import type { Server } from 'node:http'; import request from 'supertest'; import { createRouter } from './router'; describe('createRouter', () => { let app: Server; beforeAll(async () => { const router = await createRouter(); app = await wrapServer(express().use(router)); }); // Bad: the empty object won't satisfy the required properties in the spec, // causing the OpenAPI validation proxy to fail the test. it('should not use an empty mock', async () => { const entity: Entity = {} as any; app.get('/test', () => { return entity; }); const response = await request(app).get('/test'); expect(response.body).toEqual(entity); }); // Good: all required properties are present, so the response matches the // spec and validation passes. it('should return a valid entity', async () => { const entity: Entity = { apiVersion: 'a1', kind: 'k1', metadata: { name: 'n1' }, }; app.get('/test', () => { return entity; }); const response = await request(app).get('/test'); expect(response.body).toEqual(entity); }); });这个例子非常直观地展示了测试的价值:第一个用例返回空对象({} as any),无法满足规范中实体的必填属性,校验代理会直接让测试失败;第二个用例补齐了所有必填属性,校验通过。
校验失败时的处理路径
当发现校验错误时,通常有两种解决方式:
- 手工修正规范——通常适用于请求体或响应体发生变化的情形;
- 修正测试用例——确保测试返回完整的、填充好的返回值。
从测试基础设施看,@backstage/backend-openapi-utils/testUtils对应的实现位于 packages/backend-openapi-utils/src/testUtils.ts,而请求体、响应体、参数的逐项校验逻辑分别实现在 packages/backend-openapi-utils/src/schema/request-body-validation.ts、packages/backend-openapi-utils/src/schema/response-body-validation.ts 与 packages/backend-openapi-utils/src/schema/parameter-validation.ts 中,每个模块都配有对应的*.test.ts测试文件,可以作为理解校验规则的参考。
完整工作流小结
一个典型的 Schema-First 开发循环如下:
- 在后端插件中创建
src/schema/openapi.yaml,编写(或从 3.0 迁移到)OpenAPI 3.1 规范; - 运行
yarn backstage-repo-tools package schema openapi validate确认规范结构合法,必要时运行repo schema openapi lint检查风格; - 运行
yarn backstage-repo-tools package schema openapi generate --server生成类型化 Router,并在createRouter.ts中接入; - 运行
yarn backstage-repo-tools package schema openapi generate --client-package plugins/<plugin-name>-common/client生成客户端,在CatalogClient之类的高层客户端中封装DefaultApiClient; - 在测试中通过
wrapServer包裹测试服务,让测试流量反向校验规范与实现的同步性; - (可选)借助
package schema openapi diff在 CI 中检测规范的破坏性变更——该命令需要oasdiffCLI 与java环境。
至此,你的 OpenAPI 规范就不再只是一份"文档",而是贯穿插件后端类型、客户端代码与测试验证全流程的可执行契约。
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考