OpenAPI-First #2: Xây dựng Spec Repo cho nhiều repo
Khi có nhiều repo cùng dùng một API, việc mỗi repo tự generate từ OpenAPI dẫn đến drift. Bài này hướng dẫn cách tạo một spec repo trung tâm publish npm package cho cả team.
Nghe bài viết này dưới dạng podcastNếu bạn có NestJS repo và Next.js repo cùng gọi một API, câu hỏi đặt ra là: ai giữ openapi.yaml? Ai chạy generate? Làm sao đảm bảo cả hai đều dùng cùng một version contract?
Giải pháp: tách ra một spec repo riêng, đóng gói output thành npm packages, publish lên registry. Hai repo kia chỉ cần pnpm install.
Kiến trúc tổng thể
api-spec/ (repo này)
├── openapi.yaml ← SOURCE OF TRUTH
├── packages/
│ ├── nestjs-contracts/ ← @your-org/api-contracts-nestjs
│ └── client-types/ ← @your-org/api-contracts-client
└── .github/workflows/ ← CI tự động publish
NestJS repo
└── pnpm add @your-org/api-contracts-nestjs@1.2.0
Next.js repo
└── pnpm add @your-org/api-contracts-client@1.2.0
Setup pnpm workspace
# pnpm-workspace.yaml
packages:
- 'packages/*'
// package.json (root)
{
"scripts": {
"generate:all": "pnpm --filter './packages/*' generate",
"build:all": "pnpm --filter './packages/*' build",
"validate": "openapi-generator-cli validate -i openapi.yaml"
}
}
Package 1: nestjs-contracts
Generate DTOs và interfaces cho NestJS:
// packages/nestjs-contracts/package.json
{
"name": "@your-org/api-contracts-nestjs",
"version": "1.0.0",
"scripts": {
"generate": "openapi-generator-cli generate -i ../../openapi.yaml -g typescript-nestjs -o src/generated",
"build": "tsup src/index.ts --format cjs,esm --dts",
"prepublishOnly": "pnpm generate && pnpm build"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com",
"access": "restricted"
}
}
Export từ package:
// packages/nestjs-contracts/src/index.ts
// DTOs được generate từ openapi.yaml
export * from './generated/dto/create-user.dto';
export * from './generated/dto/update-user.dto';
export * from './generated/dto/user-response.dto';
export * from './generated/dto/user-list-response.dto';
// Interfaces để consumer implement
export * from './interfaces/users.interface';
export * from './interfaces/auth.interface';
Package 2: client-types
Generate TypeScript types và typed fetch client cho frontend:
// packages/client-types/package.json
{
"name": "@your-org/api-contracts-client",
"version": "1.0.0",
"scripts": {
"generate": "openapi-typescript ../../openapi.yaml -o src/generated/schema.d.ts",
"build": "tsup src/index.ts --format cjs,esm --dts"
},
"dependencies": {
"openapi-fetch": "^0.10.0"
}
}
// packages/client-types/src/index.ts
export type { paths, components, operations } from './generated/schema';
export { createApiClient } from './client';
// packages/client-types/src/client.ts
import createClient from 'openapi-fetch';
import type { paths } from './generated/schema';
export function createApiClient(baseUrl: string) {
return createClient<paths>({ baseUrl });
}
Generate scripts
# scripts/generate-nestjs.sh
#!/bin/bash
set -e
rm -rf packages/nestjs-contracts/src/generated
openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-nestjs \
-o packages/nestjs-contracts/src/generated \
--additional-properties=fileNaming=kebab-case
echo "✅ NestJS contracts generated"
# scripts/generate-client.sh
#!/bin/bash
set -e
npx openapi-typescript openapi.yaml \
-o packages/client-types/src/generated/schema.d.ts
echo "✅ Client types generated"
Setup GitHub Packages registry
# .npmrc (commit vào repo)
@your-org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
Trong GitHub repo settings, thêm secret NPM_TOKEN với value là GitHub Personal Access Token có quyền write:packages.
Kết luận
- Spec repo giải quyết vấn đề "ai giữ openapi.yaml" khi có nhiều repo.
- Hai packages riêng biệt: một cho NestJS (DTO + interface), một cho frontend (types + client).
- Consumer repo không cần biết OpenAPI tồn tại — chỉ cần
pnpm install. - Registry GitHub Packages phù hợp cho private packages trong organization.
prepublishOnlyscript đảm bảo luôn generate + build trước khi publish.
Chưa có bình luận
Để lại bình luận
Bình luận sẽ được phê duyệt trước khi hiển thị.