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 podcast

Nế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.
  • prepublishOnly script đả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ị.

Nhận bài viết mới

Mình sẽ gửi email khi có bài mới. Không spam.