Backstage 插件 OpenAPI Schema-First 开发实战:从规范到类型化 Router 与自动生成 Client
2026/9/12 6:46:53 网站建设 项目流程

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 规范与插件生命周期更紧密地耦合,具体提供三类能力:

  1. 类型化的 Express Router:根据规范生成带强类型约束的expressRouter,在开发阶段为输入与输出值提供强力护栏(guardrails)。支持 query、path 参数和 request body 的完整类型推导,同时对 headers 与 cookies 提供实验性支持。
  2. 自动生成的客户端:基于规范生成与插件后端交互的客户端代码,覆盖所有请求类型、参数、body 与返回类型,并提供低层接口以便更高层的库在此基础上做定制。
  3. 校验与验证工具:确保 API 实现与规范始终保持同步,包括在单元测试阶段对请求/响应进行实体验证。

从源码结构看,这一系列命令集中在 packages/repo-tools/src/commands/package/schema/openapi(面向单个插件包)与 packages/repo-tools/src/commands/repo/schema/openapi(面向整个仓库)两个目录下,分别提供generatevalidatedifffuzzlint等子命令。

前置条件

技术要求

本教程假定你已经具备以下基础:

  1. 会构建一个 Backstage 插件;
  2. 熟悉Express.jsTypeScript
  3. 了解 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.tscreateRouter.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 可以看到生成过程的完整链路:

  1. 读取src/schema/openapi.yaml,通过@openapitools/openapi-generator-clitypescript生成器配合仓库内模板 templates/typescript-backstage-server.yaml 生成基础类型与EndpointMap
  2. 生成src/schema/openapi/generated/router.ts,其核心是调用@backstage/backend-openapi-utils包导出的createValidatedOpenApiRouterFromGeneratedEndpointMap<EndpointMap>(spec, options),其中specJSON.stringify(yaml, null, 2) as const形式内嵌;
  3. 自动生成src/schema/openapi/index.ts(内容为export * from './generated'),因此插件侧只需要import { createOpenApiRouter } from '../schema/openapi'即可;
  4. 生成后会自动执行 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,生成客户端前需要满足两点:

  1. 将 OpenAPI 文件的info.title设置为你的 pluginId,例如:
info: # your pluginId title: catalog
  1. 找到或新建一个用于承载生成客户端代码的插件包。目前工具不支持生成一个全新插件,只会生成客户端文件。

在现有 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.tsrouter.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),无法满足规范中实体的必填属性,校验代理会直接让测试失败;第二个用例补齐了所有必填属性,校验通过。

校验失败时的处理路径

当发现校验错误时,通常有两种解决方式:

  1. 手工修正规范——通常适用于请求体或响应体发生变化的情形;
  2. 修正测试用例——确保测试返回完整的、填充好的返回值。

从测试基础设施看,@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 开发循环如下:

  1. 在后端插件中创建src/schema/openapi.yaml,编写(或从 3.0 迁移到)OpenAPI 3.1 规范;
  2. 运行yarn backstage-repo-tools package schema openapi validate确认规范结构合法,必要时运行repo schema openapi lint检查风格;
  3. 运行yarn backstage-repo-tools package schema openapi generate --server生成类型化 Router,并在createRouter.ts中接入;
  4. 运行yarn backstage-repo-tools package schema openapi generate --client-package plugins/<plugin-name>-common/client生成客户端,在CatalogClient之类的高层客户端中封装DefaultApiClient
  5. 在测试中通过wrapServer包裹测试服务,让测试流量反向校验规范与实现的同步性;
  6. (可选)借助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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询