本文基于以下两个项目当前未提交代码整理:
/Users/lili/company/common/auth-svc/Users/lili/company/ota/ota-admin-frontend
auth-svc 新增一个基于 FastAPI OpenAPI schema 生成的 TypeScript 类型包,前端通过该类型包消费后端接口契约。
当前阶段只接入类型,不生成请求 client,也不改变 ota-admin-frontend 现有的 request 封装。

后端项目新增 OpenAPI 导出脚本与类型包目录。
scripts/export_openapi.py
contracts/auth-svc-types/README.md
contracts/auth-svc-types/package.json
contracts/auth-svc-types/pnpm-lock.yaml
contracts/auth-svc-types/openapi.json
contracts/auth-svc-types/index.d.ts
.gitignore
主要变化:
scripts/export_openapi.py,通过 app.openapi() 导出后端 OpenAPI schema。contracts/auth-svc-types 类型包,包名为 @tripguru/auth-svc-types。openapi-typescript 将 openapi.json 生成 index.d.ts。.gitignore 增加 node_modules,避免类型包安装依赖后误提交。当前生成的 OpenAPI 信息:
openapi: 3.1.0
title: auth-svc
version: 0.1.0
前端项目开始消费 auth-svc 生成的类型。
package.json
pnpm-lock.yaml
src/service/api/auth.ts
src/typings/api/auth-svc.d.ts
主要变化:
package.json 增加 @tripguru/auth-svc-types 依赖,当前使用本地 link: 指向 auth-svc 的类型包目录。src/typings/api/auth-svc.d.ts 新增 Api.AuthSvc 命名空间,用于封装 OpenAPI 生成类型。src/service/api/auth.ts 引入 components,并将 fetchLogin 的请求参数和返回值替换为后端生成类型。Api.Auth 手写类型。在 auth-svc 项目根目录执行:
uv run python scripts/export_openapi.py
执行后生成:
contracts/auth-svc-types/openapi.json
该文件来自 FastAPI 的 app.openapi(),是类型生成的契约源。
脚本内容:
import json
from pathlib import Path
from app.main import app
def main() -> None:
output_path = Path('contracts/auth-svc-types/openapi.json')
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(
json.dumps(app.openapi(), ensure_ascii=False, indent=2),
encoding='utf-8',
)
if __name__ == '__main__':
main()
进入类型包目录:
cd contracts/auth-svc-types
pnpm install
当前类型包使用:
openapi-typescript: ^7.10.1
pnpm build
执行后生成:
contracts/auth-svc-types/index.d.ts
index.d.ts 是自动生成文件,不应手动修改。
npm pack --dry-run
期望发布内容保持精简:
README.md
index.d.ts
openapi.json
package.json
当前 ota-admin-frontend 使用本地 link 联调:
{
"@tripguru/auth-svc-types": "link:/Users/lili/company/common/auth-svc/contracts/auth-svc-types"
}
正式发布后应改为 npm 包版本:
pnpm add -D @tripguru/auth-svc-types
建议前端不要在业务代码里到处直接书写 components['schemas']['xxx'],而是先封装业务别名。
当前新增的封装示例:
import type { components, operations, paths } from '@tripguru/auth-svc-types';
declare namespace Api {
namespace AuthSvc {
type Schemas = components['schemas'];
type Operations = operations;
type Paths = paths;
type LoginRequest = Schemas['LoginRequest'];
type TokenResponse = Schemas['TokenResponse'];
type UserInfoResponse = Schemas['UserInfoResponse'];
type RefreshTokenRequest = Schemas['RefreshTokenRequest'];
type VerificationCodeSendRequest = Schemas['VerificationCodeSendRequest'];
}
}
fetchLogin 已从原来的手写类型切换到 OpenAPI 生成类型:
import type { components } from '@tripguru/auth-svc-types';
type LoginRequest = components['schemas']['LoginRequest'];
type LoginResponse = components['schemas']['TokenResponse'];
export function fetchLogin(params: LoginRequest) {
return request<LoginResponse>({
baseURL: getEnv('VITE_SERVICE_BASE_URL'),
url: '/v1/auth/login',
method: 'post',
params: { loginScene: 'tenant_admin' },
data: params
});
}
这里返回值使用 TokenResponse,不是 ResponseModel_TokenResponse_。
原因是 ota-admin-frontend 的 request 封装已经在 transform 中执行了:
return response.data.data;
也就是说业务接口拿到的是后端 ResponseModel<T> 里的 data 字段,所以泛型应该传入内层业务数据类型 T。
后端生成的登录请求类型:
type LoginRequest = {
email: string;
password: string;
deviceId?: string | null;
ip?: string | null;
userAgent?: string | null;
};
后端生成的 token 响应类型:
type TokenResponse = {
accessToken: string;
expiresIn: number;
refreshToken: string;
refreshExpiresIn: number;
tokenType: 'Bearer';
tenantId?: number | null;
userId: number;
identityType: string;
roles?: string[];
scope?: string[];
};
为了保证生成出来的前端类型准确,auth-svc 后续新增或修改接口时需要遵守:
ResponseModel[T]。dict。openapi.json 和 index.d.ts。fetchGoogleLogin、fetchGoogleBind、fetchRefreshToken、sendAgentOnboardingEmailCode 等 auth 接口继续迁移到 Api.AuthSvc 类型。@tripguru/auth-svc-types 仍是本地 link:,正式环境应改为发布后的 npm 版本。contracts/auth-svc-types/package.json 的 private 字段是否应调整。