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.

Nghe bài viết này dưới dạng podcast

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.

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.