AI-Native Solutions Architect

API First Design: Cách để AI tạo OpenAPI/Swagger từ Business Logic

7/19/2026 · 12p đọc


title: "API First Design: Cách để AI tạo OpenAPI/Swagger từ Business Logic"
series: "AI-Native Solutions Architect: Từ Coder đến Kiến trúc sư AI"
season: "Season 2 — AI-Augmented Architecture & System Design"
order: 16
audience: "Software Engineer hướng tới Solutions Architect"
reading_time: "13 phút"
tags: ["api-first", "openapi", "swagger", "contract-driven-development", "ai-augmented-architecture", "api-design", "backend", "frontend-collaboration"]

API First Design: Cách để AI tạo OpenAPI/Swagger từ Business Logic

Bạn đã từng gặp cảnh này chưa: FE team hỏi "endpoint tạo booking trả về field gì khi lỗi trùng lịch?", bạn mở code BE ra tìm, thấy ba chỗ throw exception khác nhau với ba format response khác nhau, và file Swagger trên Confluence thì ghi ngày cập nhật cuối cùng là ba tháng trước. Đây không phải lỗi cá nhân — đây là hậu quả tất yếu của việc viết code trước, viết tài liệu sau.

Trong mô hình "code-first", spec API là sản phẩm phụ, sinh ra bằng cách quét code hiện có (@nestjs/swagger decorator, JSDoc, hoặc tệ hơn là viết tay). Mỗi lần logic nghiệp vụ đổi — thêm field optional, đổi status code lỗi, thêm rate limit — code chạy trước, decorator cập nhật sau (nếu ai đó nhớ), và tài liệu public-facing cập nhật sau nữa (nếu ai đó rảnh). Chuỗi trễ này tích luỹ thành một khoảng cách vĩnh viễn giữa "API mà FE nghĩ là đang gọi" và "API mà BE thực sự expose".

API-First đảo ngược trật tự: spec OpenAPI được viết và thống nhất TRƯỚC, cả BE lẫn FE code song song dựa trên cùng một hợp đồng, và spec đó — không phải code — là nguồn sự thật (source of truth). Cái mới trong bài này không phải là khái niệm API-First (đã có từ lâu, các công ty như Amazon áp dụng triết lý "API as a product" từ hơn một thập kỷ trước), mà là cách AI hạ thấp chi phí viết spec xuống gần bằng chi phí mô tả nghiệp vụ bằng lời — khiến rào cản lớn nhất của API-First (viết YAML tốn thời gian) gần như biến mất.

Vấn đề

Xét một tình huống cụ thể trong một hệ thống CRM/booking: Product Manager mô tả yêu cầu "khách hàng tạo booking mới, cần chọn dịch vụ, ngày giờ, và số lượng người tham gia". Với quy trình code-first truyền thống, chuyện gì xảy ra tiếp theo thường là:

  1. BE engineer nhận yêu cầu, tự suy diễn field names (serviceId hay service_id? startTime hay bookingDate + bookingTime?), tự quyết định status code lỗi (409 hay 422 khi trùng lịch?), rồi code thẳng vào controller.
  2. FE engineer, vì cần bắt đầu code UI song song để kịp deadline, tự đoán shape của request/response dựa trên Figma và kinh nghiệm cũ, mock data theo phỏng đoán riêng.
  3. Đến lúc tích hợp (thường là gần cuối sprint), hai bên phát hiện field không khớp: BE trả status: "CONFIRMED", FE mong đợi bookingStatus: "confirmed". Cả hai phải refactor, một bên chịu thiệt.
  4. Swagger doc được sinh ra bằng decorator sau khi code đã "chốt", nhưng không ai review lại xem decorator có phản ánh đúng validation logic thực tế (ví dụ maxLength của ghi chú, hay rule "participants tối thiểu 1, tối đa theo capacity của dịch vụ") hay không.

Chi phí thực của mô hình này không nằm ở công sức viết tài liệu — nó nằm ở việc hai đội phát triển song song trên hai giả định khác nhau, rồi trả giá bằng thời gian tích hợp và bug production khi giả định sai. Trong một hệ thống nhiều team (BE, FE, mobile, đối tác tích hợp qua public API), chi phí này nhân lên theo số lượng consumer.

API-First không loại bỏ hoàn toàn rủi ro sai lệch — nhưng nó chuyển thời điểm phát hiện sai lệch từ "lúc tích hợp" sang "lúc review spec", khi sửa một dòng YAML rẻ hơn nhiều so với sửa code đã viết ở cả hai phía.

Kỹ thuật cốt lõi

API-First có ba trụ cột kỹ thuật, và AI tác động trực tiếp vào trụ cột tốn thời gian nhất.

Trụ cột 1 — Spec là hợp đồng, không phải tài liệu. OpenAPI spec (định dạng YAML hoặc JSON theo chuẩn OpenAPI Specification, thường gọi tắt là "Swagger spec" theo tên công cụ khởi nguồn) mô tả đầy đủ: đường dẫn, method, request schema, response schema cho từng status code, error format, security scheme. Điểm khác biệt so với code-first: spec này được review và approve TRƯỚC khi dòng code đầu tiên của implementation được viết.

Trụ cột 2 — Codegen hai chiều. Từ một spec đã chốt, có thể sinh: server stub (controller skeleton, DTO/interface types) cho BE, và client SDK (typed fetch functions, TypeScript types) cho FE. Cả hai team code trên cùng một schema, loại bỏ khả năng lệch field name hay kiểu dữ liệu ở tầng biên dịch (compile-time) thay vì phát hiện ở tầng tích hợp (integration-time).

Trụ cột 3 — AI làm cầu nối biz-logic → spec. Đây là phần thực sự thay đổi cuộc chơi so với 5 năm trước. Trước kia, viết OpenAPI spec tay đòi hỏi biết cú pháp YAML của spec (schema, $ref, oneOf, discriminator...) — một rào cản kỹ thuật khiến nhiều team bỏ qua bước "viết spec trước" và quay về code-first cho nhanh. AI (LLM) có thể nhận một mô tả nghiệp vụ bằng ngôn ngữ tự nhiên và sinh ra spec hợp lệ về mặt cú pháp gần như ngay lập tức, hạ chi phí viết spec xuống mức tương đương viết một đoạn mô tả yêu cầu.

Điều quan trọng cần nhấn mạnh: AI sinh spec nhanh, nhưng spec đúng cú pháp không có nghĩa là spec đúng với hệ thống thật. Vai trò của kiến trúc sư không biến mất — nó chuyển từ "gõ YAML" sang "review và đối chiếu spec AI sinh ra với ràng buộc kỹ thuật thật" (data type của DB hiện có, rate limit của gateway, giới hạn của message queue nếu endpoint là async...). Quy trình đề xuất:

flowchart LR
    A["Business requirement<br/>(ngôn ngữ tự nhiên)"] --> B["AI sinh draft<br/>OpenAPI YAML"]
    B --> C{"Architect review<br/>đối chiếu ràng buộc<br/>hệ thống thật"}
    C -- "phát hiện lệch" --> B
    C -- "đạt chuẩn" --> D["Spec chốt<br/>(source of truth)"]
    D --> E["BE: codegen<br/>controller/DTO stub"]
    D --> F["FE: codegen<br/>typed client SDK"]
    E --> G["Code song song,<br/>test theo contract"]
    F --> G

Vòng lặp "AI sinh draft → architect review → sửa" có thể lặp 2-3 lần cho một endpoint phức tạp, nhưng vẫn nhanh hơn nhiều so với việc kiến trúc sư tự gõ YAML từ đầu.

Thực hành

Bước 1 — Prompt mẫu yêu cầu AI sinh OpenAPI spec từ mô tả nghiệp vụ

Đây là điểm mấu chốt: prompt phải cung cấp đủ ngữ cảnh kỹ thuật (không chỉ mô tả nghiệp vụ suông), nếu không AI sẽ bịa ra các ràng buộc hợp lý-nghe-được nhưng sai với hệ thống thật. Dưới đây là prompt mẫu dùng để sinh spec cho endpoint "tạo booking mới":

Bạn là API architect. Hãy sinh một đoạn OpenAPI 3.1 spec (định dạng YAML)
cho MỘT endpoint duy nhất, dựa trên mô tả nghiệp vụ và ràng buộc kỹ thuật sau.

MÔ TẢ NGHIỆP VỤ:
- Endpoint: khách hàng (đã đăng nhập) tạo một booking mới cho một dịch vụ.
- Input bắt buộc: serviceId (ID dịch vụ), scheduledAt (thời điểm bắt đầu),
  participantsCount (số người tham gia).
- Input tuỳ chọn: note (ghi chú của khách hàng, tối đa 500 ký tự).
- Business rule: participantsCount phải >= 1 và <= capacity của dịch vụ
  (capacity lấy từ bảng services, không truyền vào request).
- Business rule: không cho phép tạo booking nếu đã có booking khác của
  CÙNG service trùng khung giờ (conflict theo scheduledAt + duration
  của service) — trả lỗi 409 nếu trùng.
- Business rule: scheduledAt phải là thời điểm trong tương lai (ít nhất
  30 phút kể từ thời điểm gọi API) — trả lỗi 422 nếu vi phạm.
- Response thành công: trả về booking vừa tạo, gồm id, status (giá trị
  enum: PENDING, CONFIRMED, CANCELLED), serviceId, scheduledAt,
  participantsCount, createdAt.

RÀNG BUỘC KỸ THUẬT CỦA HỆ THỐNG (BẮT BUỘC tuân theo, không tự suy diễn khác):
- Auth: Bearer JWT, header Authorization, verify qua middleware hiện có.
- Database: cột participants_count trong MySQL là TINYINT UNSIGNED
  (giới hạn cứng 0-255) — schema phải validate maximum tương ứng.
- Rate limit: endpoint POST đang bị giới hạn 20 requests/phút/user qua
  API Gateway (Redis-based) — spec cần mô tả trong description và
  response 429.
- Định dạng lỗi chuẩn của hệ thống (áp dụng cho MỌI mã lỗi >= 400):
  { "statusCode": number, "message": string, "errorCode": string,
    "timestamp": string (ISO 8601) }
- Tên field trong JSON dùng camelCase (khớp entity TypeORM hiện có),
  KHÔNG dùng snake_case dù DB column là snake_case.
- Không tự thêm field nào ngoài mô tả nghiệp vụ ở trên. Nếu thấy mô tả
  còn thiếu field hoặc rule quan trọng (ví dụ: idempotency key, phân
  trang, versioning), liệt kê thành danh sách "CÂU HỎI CẦN LÀM RÕ" ở
  cuối câu trả lời thay vì tự đoán.

Xuất kết quả gồm 2 phần: (1) đoạn YAML OpenAPI hoàn chỉnh cho path này
(bao gồm components/schemas dùng $ref), (2) danh sách câu hỏi cần làm rõ (nếu có).

Lưu ý cấu trúc prompt: phần "RÀNG BUỘC KỸ THUẬT" không phải là chi tiết thừa — đây là phần quyết định spec có dùng được hay không. Yêu cầu cuối cùng ("liệt kê câu hỏi cần làm rõ thay vì tự đoán") là kỹ thuật quan trọng để chặn AI "điền vào chỗ trống" bằng phỏng đoán hợp lý nhưng sai.

Bước 2 — Spec YAML kết quả (minh hoạ)

Đây là dạng output kỳ vọng từ prompt trên — dùng làm baseline để review, không phải để copy nguyên xi cho hệ thống khác:

openapi: 3.1.0
info:
  title: Booking Service API
  version: 1.0.0

paths:
  /bookings:
    post:
      operationId: createBooking
      summary: Tạo booking mới cho một dịch vụ
      security:
        - bearerAuth: []
      description: >
        Tạo booking cho user đã xác thực. Giới hạn 20 requests/phút/user
        (rate limit áp dụng qua API Gateway, trả 429 nếu vượt).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBookingRequest'
      responses:
        '201':
          description: Booking được tạo thành công
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookingResponse'
        '401':
          description: Thiếu hoặc sai JWT token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Trùng lịch với booking khác của cùng dịch vụ
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: >
            Vi phạm business rule (scheduledAt không hợp lệ, hoặc
            participantsCount vượt capacity của dịch vụ)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Vượt rate limit (20 req/phút/user)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    CreateBookingRequest:
      type: object
      required:
        - serviceId
        - scheduledAt
        - participantsCount
      properties:
        serviceId:
          type: string
          format: uuid
          description: ID của dịch vụ được booking
        scheduledAt:
          type: string
          format: date-time
          description: >
            Thời điểm bắt đầu booking (ISO 8601), phải cách thời điểm
            gọi API tối thiểu 30 phút
        participantsCount:
          type: integer
          minimum: 1
          maximum: 255
          description: >
            Số người tham gia. Giới hạn cứng 255 do cột DB là
            TINYINT UNSIGNED; giới hạn thực tế theo capacity dịch vụ
            được validate ở tầng application, không phải ở schema này.
        note:
          type: string
          maxLength: 500
          nullable: true

    BookingResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [PENDING, CONFIRMED, CANCELLED]
        serviceId:
          type: string
          format: uuid
        scheduledAt:
          type: string
          format: date-time
        participantsCount:
          type: integer
        createdAt:
          type: string
          format: date-time

    ErrorResponse:
      type: object
      properties:
        statusCode:
          type: integer
        message:
          type: string
        errorCode:
          type: string
        timestamp:
          type: string
          format: date-time

Chú ý cách spec này thể hiện rõ ràng buộc kỹ thuật thật (comment maximum: 255 gắn với lý do "cột DB là TINYINT UNSIGNED") thay vì chỉ nói chung chung "giới hạn hợp lý". Đây là dấu hiệu cho thấy prompt đã cung cấp đủ ngữ cảnh — nếu spec AI sinh ra không có những chi tiết bám sát hệ thống thật kiểu này, đó là tín hiệu cần bổ sung ngữ cảnh và sinh lại.

Bước 3 — Đưa spec vào quy trình làm việc thực tế

Sau khi spec được chốt (qua review giữa BE lead, FE lead, và kiến trúc sư), quy trình thực thi trong một stack NestJS + Next.js điển hình:

  • BE: dùng spec làm nguồn tham chiếu để viết DTO (class-validator decorator phải khớp minimum/maximum/maxLength trong spec) và controller (@nestjs/swagger decorator để spec runtime khớp spec đã chốt — không phải sinh spec runtime rồi coi đó là "chân lý mới").
  • FE: dùng codegen tool (ví dụ openapi-typescript hoặc tương đương) để sinh type definitions từ spec YAML, dùng trực tiếp trong zod schema hoặc type cho react-hook-form.
  • CI: thêm bước kiểm tra spec runtime (sinh từ decorator khi service chạy) không lệch khỏi spec đã chốt trong repo — phát hiện "code đã âm thầm lệch hợp đồng" trước khi merge, thay vì để FE phát hiện lúc tích hợp.

Cạm bẫy thường gặp

Spec nghe hợp lý nhưng sai với ràng buộc kỹ thuật thật. Đây là rủi ro lớn nhất và là lý do bài này nhấn mạnh "Ràng buộc kỹ thuật" trong prompt. Nếu bạn chỉ đưa AI mô tả nghiệp vụ suông ("tạo booking với serviceId, thời gian, số người"), AI sẽ tự chọn kiểu dữ liệu "hợp lý" — ví dụ participantsCount không giới hạn, hoặc rate limit không được nhắc tới — và spec trông hoàn toàn chuyên nghiệp trong khi âm thầm vi phạm giới hạn TINYINT của cột DB hiện có, hoặc bỏ sót response 429 mà API Gateway thực tế sẽ trả về. Luôn đối chiếu spec với: kiểu dữ liệu cột DB thật, rate limit tầng gateway, format lỗi chuẩn hệ thống, security scheme thật — trước khi coi spec là "chốt".

Đồng bộ hai chiều bị đứt gãy theo thời gian. Spec chốt hôm nay, nhưng nếu team không có cơ chế CI kiểm tra spec runtime (sinh từ decorator) khớp với spec trong repo, sáu tháng sau bạn quay lại đúng vấn đề ban đầu: code lệch tài liệu, chỉ khác là bây giờ "tài liệu" trông chuyên nghiệp hơn (có YAML, có Swagger UI) nên người ta tin tưởng nó hơn — và vì vậy hậu quả khi nó sai còn nặng hơn tài liệu Confluence cũ mà ai cũng biết là "có thể lỗi thời".

Lạm dụng AI generate cho toàn bộ spec lớn cùng lúc. Sinh một file OpenAPI cho 30 endpoint cùng lúc từ một mô tả nghiệp vụ dài dòng khiến việc review trở nên bất khả thi — không ai đọc kỹ hết 2000 dòng YAML để bắt lỗi. Nên sinh và review từng endpoint hoặc từng nhóm endpoint liên quan (ví dụ toàn bộ CRUD của một resource), giữ vòng lặp review đủ nhỏ để thực sự đối chiếu được với ràng buộc thật.

🧭 Góc nhìn Solutions Architect
Spec này có thể tồn tại độc lập với code hiện tại không, hay nó chỉ là "code hiện tại viết lại bằng YAML"? Nếu ba team (BE, FE, đối tác thứ ba) code song song từ spec này, điểm nào dễ gây hiểu lầm nhất — và điểm đó có được ghi rõ trong description hay bị bỏ ngỏ cho suy diễn? Nếu business rule đổi tuần sau, quy trình cập nhật spec → codegen lại → review diff có tốn ít hơn quy trình sửa code trực tiếp ở cả hai phía không — nếu không, API-First chỉ là thêm một bước thủ tục.

🔗 Bài viết liên quan

  • [Bài 15] Event-Driven Design với AI hỗ trợ — khi hợp đồng giữa các service không phải là REST endpoint mà là event schema, nguyên tắc "spec trước, code sau" áp dụng tương tự cho message payload.
  • [Bài 20] AI Integration Patterns — những LLM/Agents — khi hệ thống của bạn expose API cho AI agent gọi (không phải người hay FE), OpenAPI spec càng quan trọng vì agent dựa hoàn toàn vào spec để suy luận cách gọi đúng.

Bài trước: Event-Driven Design · Bài tiếp theo: Infrastructure as Code (IaC)

API First Design: Cách để AI tạo OpenAPI/Swagger từ Business Logic