OpenAPI-First #1: Tại sao spec phải là source of truth

Hầu hết team viết code trước rồi mới viết doc — đây chính là nguyên nhân gây ra contract drift giữa backend và frontend. Bài này giải thích tại sao OpenAPI spec phải là nơi bắt đầu.

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

Bạn đã bao giờ gặp tình huống frontend gọi API đúng endpoint nhưng vẫn bị lỗi vì backend vừa đổi tên field mà không báo? Đây là vấn đề contract drift — và OpenAPI-First là cách giải quyết tận gốc.

Contract drift là gì?

Contract drift xảy ra khi code thực tế lệch khỏi tài liệu API. Nguyên nhân phổ biến nhất: team viết code trước, viết doc sau — hoặc tệ hơn, không viết doc.

Hệ quả:

  • Frontend và backend phải communicate liên tục để sync
  • Bug chỉ phát hiện ở runtime, không phải compile time
  • Onboarding developer mới mất nhiều thời gian hơn
  • Refactor API trở thành ác mộng

OpenAPI-First là gì?

OpenAPI-First là nguyên tắc: openapi.yaml là nơi duy nhất định nghĩa API contract. Mọi thứ khác — code, doc, client, test — đều được generate hoặc validate từ file này.

openapi.yaml (source of truth)
    ├── generate → NestJS DTOs + Interfaces
    ├── generate → TypeScript types cho frontend
    ├── generate → API documentation
    └── validate → contract check tự động

Thay vì:

Code → (hy vọng) → Documentation

Ta làm:

Specification → Code + Documentation + Tests

Tại sao không chỉ dùng TypeScript types chia sẻ?

Một câu hỏi phổ biến: "Tại sao không chỉ share TypeScript types giữa frontend và backend cho đơn giản?"

Vấn đề là TypeScript types không encode được toàn bộ HTTP contract:

// TypeScript type — không biết đây là GET hay POST
// không biết field nào là required trong query vs body
// không biết HTTP status code trả về là gì
type CreateUserDto = {
  name: string;
  email: string;
}
# OpenAPI — encode đầy đủ HTTP contract
paths:
  /users:
    post:
      requestBody:
        required: true  # ← bắt buộc
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserDto'
      responses:
        '201':          # ← status code cụ thể
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'
        '400':          # ← error case
          ...

OpenAPI encode được: HTTP method, path params, query params, request body, response shape theo từng status code, authentication requirements — TypeScript types không làm được.

Ba lợi ích cụ thể khi áp dụng

1. Phát hiện breaking change ở compile time

Khi bạn đổi name: string thành fullName: string trong spec, mọi nơi dùng field name sẽ báo lỗi TypeScript ngay — không cần chờ đến runtime hay QA.

2. Frontend và backend làm việc song song

Khi spec đã được thống nhất, frontend có thể dùng mock server generate từ spec để develop song song với backend — không cần chờ API thật.

3. Documentation luôn đúng

Doc được generate từ code, không phải viết tay — nên không bao giờ outdated.

Cách bắt đầu

Bước 1: Viết openapi.yaml trước khi viết bất kỳ dòng code nào.

Bước 2: Setup validation trong CI:

npx @openapitools/openapi-generator-cli validate -i openapi.yaml

Bước 3: Generate DTOs và TypeScript types từ spec:

# Generate TypeScript types cho frontend
npx openapi-typescript openapi.yaml -o schema.d.ts

# Generate DTOs cho NestJS
openapi-generator-cli generate -g typescript-nestjs -i openapi.yaml --model-package dto

Bước 4: Dùng tsc --noEmit để check contract tự động — CI fail nếu controller không implement đúng interface.

Kết luận

  • Contract drift là nguồn gốc của nhiều bug và friction giữa frontend/backend.
  • OpenAPI-First giải quyết bằng cách đặt spec làm trung tâm, generate mọi thứ từ đó.
  • TypeScript types không thể thay thế OpenAPI vì không encode được đầy đủ HTTP contract.
  • Lợi ích lớn nhất: phát hiện breaking change ở compile time, không phải runtime.
  • Cơ chế check: tsc --noEmit — compiler làm toàn bộ việc, không cần tool thêm.