OpenAPI-First #5: Contract Check tự động với tsc và Husky

Làm thế nào để đảm bảo controller luôn implement đúng và đủ interface từ spec? Bài này hướng dẫn setup contract-check.ts và tích hợp vào pre-commit hook và CI.

3 phút đọc
Tập này đang được chuẩn bị, quay lại sau nhé.

Bạn đã có Interface từ spec package, controller đã implements interface — nhưng làm thế nào để đảm bảo không ai quên implement một method mới khi spec thay đổi?

Giải pháp: một file contract-check.ts duy nhất, kết hợp với tsc --noEmit chạy ở pre-commit và CI.

Ý tưởng cốt lõi

TypeScript compiler là tool check contract tốt nhất
→ Tạo một file ép compiler phải check
→ Chạy tsc --noEmit ở pre-commit + CI
→ Không cần tool thêm, không cần custom script

Tạo contract-check.ts

// src/contract-check.ts
// ⚠️ File này CHỈ để TypeScript check — không import vào app
// Mỗi controller phải có một entry ở đây

import {
  IUsersController,
  IAuthController,
  IProductsController,
} from '@your-org/api-contracts-nestjs';

import { UsersController } from './features/users/users.controller';
import { AuthController } from './features/auth/auth.controller';
import { ProductsController } from './features/products/products.controller';

// Ép TypeScript kiểm tra assignability
// Nếu controller thiếu method hoặc sai signature → lỗi compile ngay đây
const _checks: [
  IUsersController,
  IAuthController,
  IProductsController,
] = [
  new UsersController(null as any),
  new AuthController(null as any),
  new ProductsController(null as any),
];

void _checks; // suppress unused warning

Lỗi trông như thế nào

Khi thiếu method:

src/contract-check.ts:18:3 - error TS2322:
Type 'UsersController' is not assignable to type 'IUsersController'.
  Property 'bulkDeleteUsers' is missing in type 'UsersController'
  but required in type 'IUsersController'.

Khi sai signature:

src/contract-check.ts:18:3 - error TS2322:
Type 'UsersController' is not assignable to type 'IUsersController'.
  Types of property 'listUsers' are incompatible.
    Type '(page: string) => Promise<UserListResponse>' is not assignable to
    Type '(page: number) => Promise<UserListResponse>'.
      Type 'string' is not assignable to type 'number'.

Lỗi chỉ thẳng vào file và dòng cụ thể — developer biết ngay phải fix gì.

Setup pre-commit hook với Husky

# Cài một lần
pnpm add -D husky
npx husky init
# .husky/pre-commit
#!/bin/sh
echo "🔍 Checking API contracts..."
pnpm contract:check

if [ $? -ne 0 ]; then
  echo "❌ Contract check failed — fix before committing"
  exit 1
fi

echo "✅ Contract check passed"
// package.json
{
  "scripts": {
    "contract:check": "tsc --noEmit",
    "prepare": "husky"
  }
}

Sau khi chạy pnpm install, Husky tự setup. Từ đó mỗi git commit đều chạy contract check trước.

Setup CI

# .github/workflows/contract-check.yml
name: Contract Check
on:
  pull_request:
    paths:
      - 'src/**'
      - 'package.json'  # khi update version package mới

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: https://npm.pkg.github.com

      - run: pnpm install
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Contract check
        run: pnpm contract:check

Quy trình đầy đủ khi có breaking change

1. api-spec thêm method mới vào IUsersController
        ↓
2. CI publish package version mới (dev tag)
        ↓
3. NestJS repo: pnpm install
        ↓
4. Dev thử git commit
   → Husky chạy tsc --noEmit
   → Báo lỗi: method missing
   → Commit bị block
        ↓
5. Dev thêm method vào UsersController
        ↓
6. git commit thành công
        ↓
7. Push → CI contract check pass → PR mergeable

Mỗi khi thêm controller mới

Quy tắc: thêm controller mới phải thêm vào contract-check.ts. Có thể enforce bằng cách thêm comment rõ ràng:

// src/contract-check.ts
// 
// QUY TẮC: Mỗi controller mới PHẢI được thêm vào đây
// trước khi merge PR. Đây là cơ chế duy nhất đảm bảo
// contract được check tự động.
//

Hoặc thêm vào PR checklist của team:

  • Nếu có controller mới, đã thêm vào src/contract-check.ts

Kết luận

  • contract-check.ts là file duy nhất cần tạo — không cần tool hay script phức tạp.
  • tsc --noEmit làm toàn bộ việc kiểm tra — TypeScript compiler là contract checker tốt nhất.
  • Pre-commit hook block commit khi contract sai — developer biết ngay, không chờ CI.
  • CI check đảm bảo không ai bypass bằng cách dùng --no-verify.
  • Mỗi controller mới phải được thêm vào contract-check.ts — đây là quy tắc team cần enforce qua code review.