Unkey Dashboard API SDK:Speakeasy 驱动的 OpenAPI 代码生成型 TypeScript SDK 与 ESM 构建实践
【免费下载链接】unkeyThe Developer Platform for Modern APIs项目地址: https://gitcode.com/GitHub_Trending/un/unkey
导读
web/internal/api是 Unkey 仓库中 dashboard 前端专用的私有 TypeScript API SDK 包(包名@unkey/api),它不是手写代码,而是由 Speakeasy 从 OpenAPI 规范自动生成的产物。本文以该包的 README 为主线,深入讲解它的生成管线(svc/api/openapi→ Speakeasy → 提交src)、重新生成工作流(mise run generate/generate-api-sdk),以及"源码提交、esm本地构建并忽略"这一特殊策略背后的 ESM 解析原理;同时结合仓库源码,剖析 SDK 的目录结构、dashboard 的实际消费方式与针对构建产物的测试方式,帮助你理解如何在 Unkey 中维护一套"规范驱动、随时可再生成"的 API 客户端。
一、这个包是什么:dashboard 内部的私有 SDK
web/internal/api/README.md开头即点明其定位:
This private workspace package is generated from
svc/api/openapi/openapi-generated.yamlusing the public SDK's Speakeasy configuration.
几个关键事实:
- 它是私有 workspace 包:
web/internal/api/package.json中声明"name": "@unkey/api"、"private": true、"type": "module",不发布到 npm 公共仓库,仅供本仓库内的 dashboard 等应用通过 workspace 依赖消费。 - 它是生成产物而非手写代码:
web/internal/api/src/index.ts等所有源码文件开头都带Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.注释,意味着人工修改会被下一次生成覆盖,规范的变更才是"唯一事实来源"。 - 生成输入是 OpenAPI 规范:其上游是 svc/api/openapi/openapi-generated.yaml,该文件为 OpenAPI 3.1.0 规范,
info.title为Unkey API、info.version为2.0.0,覆盖从/v2/analytics.getGatewayRequests到密钥、限流、部署、门户等全部平台资源路径。
因此,整个包的维护哲学是:改规范 → 重新生成 → 提交生成结果,而不是手改客户端。
二、上游 OpenAPI 规范如何产生
README 中提到的openapi-generated.yaml本身也是自动生成的产物。在 svc/api/openapi/generate.go 中可以看到完整链路:
//go:generate go run generate_bundle.go -input openapi-split.yaml -output openapi-generated.yaml //go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config=config.yaml ./openapi-generated.yaml即:
- 仓库维护的是拆分式规范
svc/api/openapi/openapi-split.yaml及spec/下的分路径 YAML(如spec/paths/v2/keys/createKey/index.yaml、spec/common/KeyCreditsRefill.yaml等),便于多人并行编辑与 review; - 通过
generate_bundle.go将拆分规范合成为单文件openapi-generated.yaml(文件头注释也写明Source: openapi-split.yaml); - Speakeasy 再基于这个 bundle 出的 OpenAPI 规范生成 TypeScript SDK。
openapi-generated.yaml顶部定义了安全方案:components.securitySchemes.bearer(bearer token),这与 SDK 中的rootKey认证能力一一对应。
三、重新生成 SDK 的标准工作流
README 给出了唯一官方入口:
Run
mise run generatefrom the repository root after changing the API specification. It invokesgenerate-api-sdk; commit the generatedsrcchanges with the specification change.
操作步骤与要点:
- 修改规范:编辑
svc/api/openapi/openapi-split.yaml或svc/api/openapi/spec/下的拆分文件; - 在仓库根目录执行:
mise run generate该任务会调用
generate-api-sdk,内部完成 bundle 合并与 Speakeasy 生成; - 同步提交:将本次规范变更与重新生成的
src/变更放在同一个 commit中提交,保证"规范 ↔ 客户端"始终一一对应、可追溯。
环境要求可参考 .mise/config.toml:仓库通过 mise 锁定工具链,其中"github:speakeasy-api/speakeasy" = "1.790.1"固定了 Speakeasy 版本,同时锁定node = "24.19.0"、"github:pnpm/pnpm" = "11.5.0"等,确保任何人(或 CI)在相同工具版本下都能复现完全一致的生成结果。
另外,SDK 元数据里也记录了生成指纹(见web/internal/api/src/lib/config.ts):
export const SDK_METADATA = { language: "typescript", openapiDocVersion: "2.0.0", sdkVersion: "2.5.1", genVersion: "2.918.1", userAgent: "speakeasy-sdk/typescript 2.5.1 2.918.1 2.0.0 @unkey/api", } as const;openapiDocVersion与规范版本、genVersion与 Speakeasy 生成器版本均可在排障时对照校验。
四、核心策略:src提交、esm忽略
README 的最后一段揭示了整个包最值得注意的设计:
The generated source is committed, while
esmis built locally or during the dashboard build and remains ignored. This ensures Next.js and Vercel consume real ESM files rather than resolving generated.jsimports against TypeScript source.
拆开来看:
src/(生成源码)纳入版本控制:所有 Speakeasy 生成的.ts文件都被提交,因此每次规范变更的 diff 都能被 code review,且 CI 无需联网重新生成即可构建;esm/(构建产物)被.gitignore忽略:web/internal/api/.gitignore中只有一行esm/;esm在本地或 dashboard 构建时生成:package.json的scripts中"build": "tsc"、"prepare": "tsc",即通过 TypeScript 编译把src/输出到esm/(tsconfig.json中outDir: "esm"、rootDir: "src");- 为什么必须消费真实 ESM:生成源码里的 import 都是
.js后缀(如export * from "./lib/config.js"),同时package.json的exports采用source/types/default三字段映射。如果让 Next.js 或 Vercel 在解析时把这些.js导入回退到 TypeScript 源文件,容易出现模块解析歧义;而预先用tsc产出真实的esm/*.js文件,能让构建链稳定地吃到 ESM 产物。
exports映射(web/internal/api/package.json)支持从@unkey/api、@unkey/api/models/errors、@unkey/api/models/components、@unkey/api/models/operations等子路径导入,dashboard 正是利用这一点按需引入类型化错误类。
五、生成产物结构深度解析
从仓库目录看,web/internal/api/src/是标准 Speakeasy TypeScript SDK 布局:
| 目录 | 内容 | 仓库中的规模/示例 |
|---|---|---|
src/funcs/ | 每个 API 操作一个调用函数 | 90+ 个文件,如keysCreateKey.ts、ratelimitLimit.ts、deploymentsCreateDeployment.ts |
src/sdk/ | 按资源分组的 SDK 模块 | analytics.ts、apis.ts、keys.ts、ratelimit.ts、portal.ts等 16 个模块 |
src/models/components/ | 请求/响应模型与枚举 | keyresponsedata.ts、ratelimitresponse.ts、deployment.ts等 180+ 文件 |
src/models/errors/ | 类型化错误类 | badrequesterrorresponse.ts、notfounderrorresponse.ts、toomanyrequestserrorresponse.ts等 |
src/models/operations/ | 分页/列表型操作类型 | keyslistkeys.ts、domainslistdomains.ts等 |
src/lib/ | 运行时基础设施 | http.ts、retries.ts、schemas.ts(zod)、security.ts、config.ts等 |
src/hooks/ | Speakeasy 钩子系统 | hooks.ts、registration.ts、types.ts |
src/types/ | 泛用类型 | rfcdate.ts、blobs.ts、streams.ts、fp.ts等 |
入口与根类:src/index.ts对外导出HTTPClient、lib/config、lib/files与sdk/sdk.js;核心类Unkey定义在 src/sdk/sdk.ts:
export class Unkey extends ClientSDK { private _keys?: Keys; get keys(): Keys { return (this._keys ??= new Keys(this._options)); } // analytics / apis / apps / deployments / domains / environments / // gateway / github / identities / internal / permissions / portal / // projects / ratelimit 同理,全部懒加载 }所有资源模块以 getter 形式懒加载,client.keys.createKey(...)、client.ratelimit.limit(...)这类调用链即来自此处。
运行时配置:src/lib/config.ts中SDKOptions支持rootKey(字符串或异步函数)、httpClient、serverIdx、serverURL、userAgent、retryConfig、timeoutMs、debugLogger,默认服务器为ServerList = ["https://api.unkey.com"]。
类型校验:依赖 zod(package.json中"zod": "catalog:",版本由 monorepo catalog 统一管理),lib/schemas.ts负责请求/响应的运行时 schema 校验,保证 SDK 返回数据与规范一致。
六、dashboard 如何消费这个 SDK
README 未展开消费端细节,但仓库代码给出了实际用法。在 web/apps/dashboard/lib/unkey-client.ts 中:
"use client"; import { Unkey } from "@unkey/api"; import * as errors from "@unkey/api/models/errors"; let client: Unkey | null = null; export function getUnkeyClient(): Unkey { if (client) { return client; } client = new Unkey({ serverURL: new URL("/proxy/", window.location.origin).toString(), }); return client; }要点:
serverURL指向同源/proxy/路径:dashboard 不直接访问https://api.unkey.com,而是通过 Next.js 的/proxy/反向代理转发 API 请求,从而隐藏根密钥、规避 CORS 与跨域问题;- 类型化错误驱动 UI:通过
@unkey/api/models/errors导入BadRequestErrorResponse、UnauthorizedErrorResponse、ForbiddenErrorResponse、NotFoundErrorResponse、ConflictErrorResponse、GoneErrorResponse、PreconditionFailedErrorResponse、UnprocessableEntityErrorResponse、TooManyRequestsErrorResponse、InternalServerErrorResponse、ServiceUnavailableErrorResponse、HTTPClientError等错误类,并用error instanceof判断映射为 toast 文案(如"Delete Protection Enabled""Permission Denied"),实现对 API 错误的结构化、可读化处理; - 组件层使用:dashboard 各处(如
use-create-key.tsx、use-delete-key.ts、use-edit-credits.ts)均import type { Unkey } from "@unkey/api",配合@tanstack/react-query的useMutation发起调用。
七、针对构建产物的测试
仓库中已有针对该 SDK 的测试示例:web/internal/api/tests/domains-list-domains.test.mjs。它使用 Node 内置的node:test与node:assert/strict,直接 import 编译后的esm/产物:
import { HTTPClient } from "../esm/lib/http.js"; import { NotFoundErrorResponse } from "../esm/models/errors/notfounderrorresponse.js"; import { Unkey } from "../esm/sdk/sdk.js";测试通过注入自定义fetcher(Response.json(body, { status: 404 }))验证两件事:
listDomains在 404 时抛出的错误是类型化的NotFoundErrorResponse,且error.error、error.meta.requestId数据完整保留;- 空的选择条件(如不存在的 project)也能正常返回空数据
data: []与分页信息pagination: { hasMore: false }。
这印证了 README 的策略:测试跑在真实 ESM 产物之上,只有先npm run build(tsc 生成esm/)才能执行,从侧面保证了"被消费的产物"而非"未编译源码"的正确性。
八、维护要点小结
结合 README 与仓库实现,维护这套 SDK 的几条核心准则可以总结为:
- 规范是唯一事实来源:一切修改从
svc/api/openapi/openapi-split.yaml(及spec/拆分文件)开始,不要手改web/internal/api/src/; - 统一入口重新生成:修改规范后在仓库根目录执行
mise run generate(内部调用generate-api-sdk),利用.mise/config.toml锁定的 Speakeasy 1.790.1 等工具版本保证可复现; - src 与规范同 commit:把规范变更和重新生成的
src/变更一起提交,便于 review 与回溯; - esm 不入库:
esm/在本地或 dashboard 构建时由tsc生成(package.json的build/prepare脚本),.gitignore忽略之,Next.js/Vercel 消费的是真实 ESM 文件; - 消费端走代理与类型化错误:dashboard 通过
new Unkey({ serverURL: "/proxy/..." })接入,并依赖@unkey/api/models/errors的类型化错误类做 UI 反馈; - 测试针对构建产物:
tests/下使用node:test直接 importesm/输出验证 SDK 行为,确保实际分发形态的正确性。
【免费下载链接】unkeyThe Developer Platform for Modern APIs项目地址: https://gitcode.com/GitHub_Trending/un/unkey
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考