Polar 内部 API 客户端 `@polar-sh/client` 深度解析:OpenAPI 驱动的类型安全前端调用层
Polar 内部 API 客户端polar-sh/client深度解析OpenAPI 驱动的类型安全前端调用层【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polarpolar-sh/client是 Polar 仓库中供前端应用使用的生成式 API 客户端它由后端 OpenAPI Schema 自动生成并基于openapi-fetchopenapi-typescript构建了完整的端到端类型安全调用链。本文将以 clients/packages/client/README.md 为骨架结合该包的源码、生成脚本、上游补丁以及前端 app 中的真实消费方式深入讲解它的设计原理、核心 API、错误处理模型与工程化最佳实践读完即可在自己的 Polar 相关前端项目中复现这套方案。一、包定位Polar 前端的「唯一 API 出口」按 clients/packages/client/README.md 的定义该包包含 Polar 前端应用内部使用的生成式 API 客户端由后端的 OpenAPI Schema 生成。它的package.jsonclients/packages/client/package.json给出了更完整的定位信息包名polar-sh/client描述为 Polar Internal API Client许可证Apache-2.0声明为private: true虽然publishConfig.access为public但本质上是仓库内部的 workspace 包产物形态同时输出dist/index.cjsCommonJS与dist/index.jsESM类型声明为dist/index.d.ts导出映射exports[.]同时提供types与default两个条件入口依赖核心openapi-fetch^0.15.0、openapi-typescript-helpers^0.0.15与date-fns用于指标时间范围计算。在clients/这个 pnpm workspace 中它被apps/appExpo 移动端、apps/webNext.js 站点等多个应用同时依赖。换句话说整个 Polar 前端的网络层都收敛在这一处后端的路由、Schema、枚举变化都只需重新生成这一个包即可同步到所有前端应用。二、从 OpenAPI 到 TypeScript 的生成流水线2.1 生成脚本做了什么package.json中的generate脚本是理解整个包的钥匙generate: uv run --directory ../../../server/ -m scripts.generate_openapi | openapi-typescript --enum-values -o ./src/v1.ts oxfmt ./src pnpm run build这条命令链做了四件事产出 OpenAPI Schema在server/目录下用uv run执行 Python 模块scripts.generate_openapi把 JSON Schema 打印到标准输出。该脚本server/scripts/generate_openapi.py通过get_openapi(version, routes_for_version(...), get_webhook_routes())汇总 FastAPI 路由与 webhook 路由并支持按 API 版本生成生成 TS 类型把 Schema 管道输入openapi-typescript --enum-values写入 clients/packages/client/src/v1.ts。--enum-values会让枚举保留为字面量联合从而支持从v1.ts中 re-export 出运行时可用的枚举值数组格式化用oxfmt统一格式化src/下所有文件构建产物执行pnpm run build即tsup产出 ESM/CJS 双格式并生成类型声明。2.2 生成产物的规模与内容生成的 src/v1.ts 是一份约 7.2 万行的自动生成文件文件头明确声明/** * This file was auto-generated by openapi-typescript. * Do not make direct changes to the file. */它导出了三组核心类型paths所有路由的请求/响应形状、componentsSchema 与枚举定义、operations按 operationId 索引的操作类型。由于文件是生成的任何手工修改都会在下次pnpm generate时被覆盖——这是该包最重要的使用约束之一。2.3 上游补丁为了让 7 万行类型“活”下来仓库里还保留了一份针对生成器的补丁 clients/patches/openapi-typescript7.10.1.patch例如给transformSchemaObject增加fromAdditionalProperties参数、给oapiRef增加deep参数。这印证了项目为了在**复杂 Schema嵌套 additionalProperties、深层引用**下正确生成类型对生成器本身做了针孔式修复也说明该客户端对类型完整性的要求极高。三、运行时客户端createClient的组装逻辑包的运行时核心在 src/index.tsexport const createClient ( baseUrl: string, token?: string, headers?: HeadersOptions, ) ({ ...createOpenAPIFetchClientpaths({ baseUrl, credentials: include, headers: { ...(headers ? headers : {}), ...(token ? { Authorization: Bearer ${token} } : {}), }, }), baseUrl, })三个入参的含义与默认行为参数类型说明baseUrlstringAPI 根地址如https://api.polar.sh必填tokenstring \| undefined可选的 Bearer Token存在时自动注入Authorization: Bearer token请求头headersHeadersOptions \| undefined额外请求头与 token 头合并内部实现基于openapi-fetch的createOpenAPIFetchClientpaths因此所有 HTTP 方法GET/POST/PATCH/DELETE 等都按路径强类型化路径、查询参数、请求体、响应体的类型全部从paths推导写错参数名或类型会在编译期直接报错。credentials: include表明客户端默认携带跨域 Cookie适合与后端 session 认证配合baseUrl也被挂在返回对象上方便调用方读取。3.1 真实调用形态以 clients/apps/app/providers/PolarClientProvider.tsx 为例移动端在 React Context 中创建客户端const polar useMemo(() { const client createClient( process.env.EXPO_PUBLIC_POLAR_SERVER_URL ?? https://api.polar.sh, session ?? , CLIENT_VERSION_HEADERS, ) client.use(refreshMiddleware) return client }, [session])要点baseUrl 可配置通过EXPO_PUBLIC_POLAR_SERVER_URL环境变量注入缺省回退到生产地址https://api.polar.shToken 来自 session登录态变化时通过useMemo依赖重建客户端版本头随请求发送CLIENT_VERSION_HEADERS携带X-Polar-Client-Version如mobile/1.0.0、X-Polar-Client-RuntimeExpo runtimeVersion、X-Polar-Client-UpdateOTA updateId方便后端区分是哪个构建在调用Middleware 扩展client.use(refreshMiddleware)挂上 openapi-fetch 的中间件实现 token 刷新逻辑。四、类型安全三件套unwrap、错误类与schemas4.1unwrap把“结果三态”收敛为“要么数据、要么抛错”openapi-fetch 的请求返回{ data, error, response }三态结构业务代码若每个请求都手动判断会非常啰嗦。src/index.ts 提供的unwrap把这个过程统一收敛export const unwrap async T, Options, Media( p: PromiseFetchResponseT, Options, Media, handlers?: { [status: number]: (response: Response) never }, ): PromiseParseAsResponseSuccessResponseResponseObjectMapT, Media, Options { const { data, error, response } await p if (handlers) { const handler handlers[response.status] if (handler) return handler(response) } if (response.status 429) { throw new TooManyRequestsResponseError({ message: Too Many Requests }, response) } if (error) { if (response.status 401) throw new UnauthorizedResponseError(error, response) else if (response.status 404) throw new NotFoundResponseError(error, response) throw new ClientResponseError(error, response) } if (!data) throw new Error(No data returned) return data }行为优先级自定义 handlers 优先可按状态码注入自定义处理返回never常用于重定向或兜底429 限流直接抛TooManyRequestsResponseError401/404 特化分别抛UnauthorizedResponseError与NotFoundResponseError其他错误统一抛ClientResponseError成功但无数据抛普通Error(No data returned)避免undefined污染类型。4.2 错误类继承体系export class ClientResponseError extends Error { error: ClientResponseErrorBody response: Response constructor(error: ClientResponseErrorBody, response: Response) { ... } } export class UnauthorizedResponseError extends ClientResponseError { ... } export class NotFoundResponseError extends ClientResponseError { ... } export class TooManyRequestsResponseError extends ClientResponseError { ... }ClientResponseErrorBody Recordstring, unknown { message?: string }允许访问后端返回的任意错误字段。所有特化错误都继承自ClientResponseError因此前端只需捕获基类即可覆盖全部错误场景同时又能按子类精确处理 401去登录或 429退避重试。4.3schemas与校验辅助export type schemas components[schemas] export type Client ReturnTypetypeof createClient export const isValidationError (detail: unknown): detail is { loc: ...; msg: string; type: string }[] ...schemas是全部业务 Schema 的类型别名供 UI 层直接引用例如 clients/apps/app/components/Metrics/utils.ts 中的schemas[Metric]、schemas[Organization]。isValidationError是类型守卫用于识别 FastAPI 风格的校验错误结构loc/msg/type数组。五、前端 hooks 中的真实用法React Query unwrapPolar 前端把客户端与 TanStack Query 深度绑定。以 clients/apps/app/hooks/polar/customers.ts 为例export const useCustomer (organizationId: string | undefined, id: string) { const { polar } usePolarClient() return useQuery({ queryKey: [customers, organizationId, { id }], queryFn: () unwrap( polar.GET(/v1/customers/{id}, { params: { path: { id } }, }), ), enabled: !!organizationId, }) }值得注意的细节路径参数强类型/v1/customers/{id}是模板字符串类型params.path.id必须匹配enabled: !!organizationId在组织未就绪时避免无效请求分页查询复用类型useCustomers通过operations[customers:list][parameters][query]直接复用后端 operationId 的查询参数类型做Omit保证前后端参数契约完全一致分页用useInfiniteQuery的pageParam驱动。同样的模式遍布 hooks/polar/checkout_links.ts、hooks/polar/custom_fields.ts、hooks/polar/finance.ts 等文件构成一套可复制的「hook 模板」。六、刷新中间件client.use 的工程化示例clients/apps/app/auth/refreshMiddleware.ts 展示了Middleware的完整用法onRequest跳过 OAuth 端点若本地 refresh token 存在且 access token 过期先refreshAccessToken()再重写Authorization头否则用最新 token 校正请求头onResponse遇 401 且不是 OAuth 端点且有 refresh token 时刷新后用新 token重放原请求options.fetch(retry)。该中间件还配有单元测试 clients/apps/app/auth/refreshMiddleware.test.ts对onRequest/onResponse的输入输出进行契约化验证可作为编写自定义 Middleware 的参考模板。七、metrics 工具把时间范围算明白src/metrics.ts 为指标 API 提供时间范围计算export type MetricsRange 24h | 30d | 3m | 12m | today | all_time export const getMetricsRangeDates (range, options?): [Date, Date] { ... }实现要点基于date-fns的subDays/subMonths/subYears/startOfDaynow可注入以便测试30d 用subDays(end, 29)注释明确解释“用 29 天而不是 30 天使指标 API 返回的含首尾两天的日桶恰好覆盖 30 天含今天”all_time必须提供createdAt如组织创建时间否则抛错。前端在 clients/apps/app/components/Metrics/utils.ts 中组合使用它构造24h/30d/3m/all_time等预设区间并依据区间跨度推导聚合粒度≥3 年用年、≥4 个月用月、4 周用周、1 天用天、否则用小时。八、构建与工程约束8.1 tsup 双格式构建clients/packages/client/tsup.config.tsexport default defineConfig([ { entry: [src/index.ts], format: [cjs, esm], minify: true, dts: process.env.POLAR_SKIP_DTS ! 1, }, ])同时产出 CJS 与 ESM满足不同消费方生产构建默认压缩设置POLAR_SKIP_DTS1可跳过类型声明生成调试加速类型声明默认开启与package.json的types字段对应。8.2 共享 tsconfigtsconfig.json 继承polar-sh/typescript-config/bundled.json见 clients/packages/typescript-config设置outDir: dist、rootDir: src仅编译src。包内还提供typecheck脚本tsc --noEmit用于 CI 校验。8.3 枚举 re-exportsrc/enums.ts 从生成的v1.ts中 re-export 一批运行时可用的枚举值数组例如orderStatusValues、checkoutStatusValues、benefitTypeValues、recurringIntervalValues、webhookEventTypeValues等。这些数组可直接用于下拉选项渲染、前端校验与参数序列化避免手写枚举与后端漂移。九、结语与延伸阅读polar-sh/client的价值可以概括为三句话单一事实来源API 契约只存在于后端的 FastAPI/OpenAPI 定义中前端类型全部自动生成杜绝手工维护 API 层端到端类型安全openapi-fetchpaths/operations/schemas类型 unwrap收敛错误让“写错接口”从运行时错误提前到编译期错误可扩展的工程基础createClient的三参数设计、Middleware钩子、可注入的now、可测试的纯函数为 token 刷新、版本上报、指标聚合等复杂场景提供了干净的扩展点。若要继续深入推荐按以下路径阅读仓库源码生成入口server/scripts/generate_openapi.py了解 Schema 如何从 FastAPI 路由与 webhook 路由汇总客户端核心clients/packages/client/src/index.ts掌握createClient/unwrap/错误类的完整实现消费示例clients/apps/app/providers/PolarClientProvider.tsx 与 clients/apps/app/hooks/polar/customers.ts对照学习在 React/Expo 中的接入方式中间件示例clients/apps/app/auth/refreshMiddleware.ts 及其测试 clients/apps/app/auth/refreshMiddleware.test.ts指标工具clients/packages/client/src/metrics.ts 与 clients/apps/app/components/Metrics/utils.ts。【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考