DEV Community

Cover image for Phát triển API theo hướng đặc tả (Spec-First) là gì?
Sebastian Petrus
Sebastian Petrus

Posted on • Originally published at apidog.com

Phát triển API theo hướng đặc tả (Spec-First) là gì?

Hầu hết lỗi API không đến từ logic code, mà đến từ hợp đồng không rõ ràng: frontend chờ userId, backend trả user_id, và vấn đề chỉ lộ ra ở QA. Phát triển API “spec-first” giải quyết việc này bằng cách viết contract trước, sau đó mới triển khai endpoint theo contract đó.

Dùng thử Apidog hôm nay

Trong bài này, bạn sẽ đi qua một workflow thực tế: viết một OpenAPI spec nhỏ, dùng chính file đó để tạo mock API, test contract và tài liệu API trước khi backend tồn tại. Cách tiếp cận này còn được gọi là spec-driven development, design-first hoặc contract-first. Điểm chung là: thống nhất giao diện trước, rồi các team cùng build theo một nguồn sự thật.

Phát triển API “spec-first” là gì?

Phát triển API “spec-first” nghĩa là bạn tạo một contract có thể đọc bằng máy, thường là file OpenAPI, trước khi triển khai endpoint.

Spec này mô tả rõ:

  • API có những path nào
  • Mỗi endpoint nhận parameter gì
  • Request body có schema nào
  • Response trả về hình dạng ra sao
  • Status code nào được phép
  • Field nào bắt buộc
  • Kiểu dữ liệu và format của từng field

Thay vì viết code trước rồi tài liệu hóa sau, spec trở thành nguồn sự thật duy nhất cho frontend, backend và QA.

Workflow thường là:

  1. Product/backend/frontend thống nhất API contract.
  2. Spec được commit vào Git.
  3. Frontend dùng mock server sinh từ spec để phát triển.
  4. Backend triển khai theo spec.
  5. QA hoặc CI chạy contract test để xác nhận implementation khớp với spec.
  6. API docs được sinh trực tiếp từ cùng file spec.

Cách này giúp đẩy lỗi tích hợp về giai đoạn thiết kế, nơi chi phí sửa thấp hơn nhiều so với khi đã có implementation.

Vòng đời “spec-first” so với “code-first”

Hai cách tiếp cận đều có thể tạo ra cùng một API, nhưng khác nhau ở thời điểm phát hiện lỗi.

Spec-first vs code-first lifecycle

Với “code-first”, các team thường phải chờ nhau:

  • Backend viết handler trước
  • Frontend chờ API thật hoặc tự giả lập response
  • QA chỉ bắt đầu kiểm thử khi backend gần hoàn tất
  • Tài liệu thường được viết sau và dễ bị lệch

Với “spec-first”, contract xuất hiện từ đầu:

  • Frontend có thể gọi mock API ngay
  • Backend biết chính xác schema cần implement
  • QA có thể chuẩn bị test case sớm
  • Docs được tạo tự động từ spec

Kết quả là các team build song song dựa trên một định nghĩa chung, thay vì đoán ý nhau qua Slack, ticket hoặc code đã deploy.

Ví dụ OpenAPI thực tế

Hãy thiết kế một endpoint /users bằng OpenAPI.

Mục tiêu:

  • GET /users: trả về danh sách user
  • POST /users: tạo user mới
  • Dùng chung schema User
  • Validate email, uuid, date-time
  • Giới hạn query limit tối đa 100

1. Khai báo metadata của API

Bắt đầu bằng phiên bản OpenAPI, thông tin API và base URL:

openapi: 3.0.3
info:
  title: Users API
  version: 1.0.0
servers:
  - url: https://api.example.com/v1
Enter fullscreen mode Exit fullscreen mode

Phần này giúp các tool biết spec đang dùng chuẩn nào và request nên được gửi đến server nào.

2. Định nghĩa schema dùng chung

Tiếp theo, định nghĩa schema User trong components/schemas:

components:
  schemas:
    User:
      type: object
      required: [id, email, createdAt]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
        createdAt:
          type: string
          format: date-time
Enter fullscreen mode Exit fullscreen mode

Ở đây:

  • id, email, createdAt là bắt buộc
  • id phải có format uuid
  • email phải có format email
  • createdAt phải là date-time

Khi schema được đặt trong components, bạn có thể tái sử dụng bằng $ref thay vì copy nhiều lần.

3. Thêm GET /users

Endpoint này trả về danh sách user và nhận query parameter limit:

paths:
  /users:
    get:
      summary: List users
      operationId: listUsers
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        "200":
          description: A list of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"
Enter fullscreen mode Exit fullscreen mode

Điểm quan trọng:

  • limit là integer
  • mặc định là 20
  • tối đa là 100
  • response 200 là một array các User

Frontend có thể dựa vào đây để biết chính xác response sẽ có dạng nào.

4. Thêm POST /users

Endpoint này tạo user mới. Client phải gửi email, còn name là tùy chọn:

    post:
      summary: Create a user
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                name:
                  type: string
      responses:
        "201":
          description: User created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          description: Invalid request body
Enter fullscreen mode Exit fullscreen mode

Contract này nói rõ:

  • Request body là bắt buộc
  • email là bắt buộc
  • Khi tạo thành công, API trả 201
  • Response thành công phải khớp schema User
  • Request không hợp lệ trả 400

5. File OpenAPI hoàn chỉnh

Ghép các phần lại, bạn có một spec đầy đủ:

openapi: 3.0.3
info:
  title: Users API
  version: 1.0.0
servers:
  - url: https://api.example.com/v1

paths:
  /users:
    get:
      summary: List users
      operationId: listUsers
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        "200":
          description: A list of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"

    post:
      summary: Create a user
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                name:
                  type: string
      responses:
        "201":
          description: User created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          description: Invalid request body

components:
  schemas:
    User:
      type: object
      required: [id, email, createdAt]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
        createdAt:
          type: string
          format: date-time
Enter fullscreen mode Exit fullscreen mode

Đến đây, chưa có dòng backend nào được viết, nhưng contract đã đủ để frontend, QA và backend bắt đầu làm việc.

Tạo mock, test và tài liệu từ spec

Lý do chính để viết spec trước là bạn có thể dùng một file duy nhất cho nhiều việc.

1. Tạo mock API

Mock server đọc OpenAPI spec và trả response theo schema đã khai báo.

Ví dụ với GET /users, mock có thể trả về dữ liệu có dạng:

[
  {
    "id": "7b9e8c4e-8e0f-4fd1-9e10-4d5a04f9b77a",
    "email": "user@example.com",
    "name": "Demo User",
    "createdAt": "2026-06-02T10:00:00Z"
  }
]
Enter fullscreen mode Exit fullscreen mode

Frontend có thể gọi mock endpoint ngay từ đầu mà không cần chờ backend.

Lợi ích thực tế:

  • UI team có dữ liệu ổn định để phát triển
  • Không cần hardcode fake JSON trong frontend
  • Khi spec đổi, mock đổi theo
  • Lỗi mismatch field được phát hiện sớm

2. Viết contract test

Spec cũng đóng vai trò như test oracle: implementation phải khớp với contract.

Ví dụ các test cần có cho POST /users:

  • Gửi body hợp lệ → nhận 201
  • Response 201 khớp schema User
  • Gửi body thiếu email → nhận 400
  • email sai format → không được tạo user
  • Response không được thiếu field bắt buộc như id hoặc createdAt

Bạn không cần tự suy đoán API nên trả gì. Test chỉ cần xác nhận backend làm đúng những gì spec đã khai báo.

3. Sinh tài liệu API

Docs được tạo trực tiếp từ OpenAPI spec.

Điều này giúp tránh tình trạng:

  • Code đã đổi nhưng docs chưa đổi
  • Docs ghi userId nhưng API trả user_id
  • Một endpoint có nhiều bản mô tả khác nhau ở nhiều nơi

Khi docs, mock và test cùng dùng một spec, contract trở thành nguồn sự thật duy nhất.

Cách này cũng phù hợp với quy trình làm việc API git-native: spec là file text, nên mọi thay đổi đều hiển thị dưới dạng diff trong pull request. Reviewer có thể phát hiện việc đổi tên field, xóa required field hoặc thay status code trước khi code được merge.

Thực hiện workflow này trong Apidog

Apidog hỗ trợ workflow này qua Chế độ Spec-First.

Thay vì xem OpenAPI như một file export sau cùng, Apidog coi spec là trung tâm của project. Bạn chỉnh sửa YAML trực tiếp, và các phần còn lại của workspace cập nhật theo.

Apidog Spec-First Mode

Một workflow thực tế có thể như sau:

  1. Tạo hoặc import OpenAPI spec.
  2. Dán spec /users vào trình chỉnh sửa.
  3. Kiểm tra lỗi schema trong editor.
  4. Bật mock server từ spec.
  5. Cho frontend gọi mock endpoint.
  6. Sinh tài liệu API từ cùng spec.
  7. Khi backend sẵn sàng, chạy request/test case dựa trên spec.
  8. So sánh response thật với schema đã khai báo.

Ví dụ, sau khi thêm GET /users, frontend có thể dùng mock URL như một API thật:

const res = await fetch("https://mock.example.com/users?limit=10");
const users = await res.json();

users.forEach((user) => {
  console.log(user.id, user.email, user.createdAt);
});
Enter fullscreen mode Exit fullscreen mode

Backend sau đó triển khai theo contract:

app.get("/users", async (req, res) => {
  const limit = Math.min(Number(req.query.limit ?? 20), 100);

  const users = await userService.list({ limit });

  res.status(200).json(users);
});
Enter fullscreen mode Exit fullscreen mode

Điểm quan trọng là cả frontend và backend đều không tự định nghĩa shape riêng. Cả hai đều bám vào spec.

Apidog cũng hỗ trợ đồng bộ Git hai chiều. Spec nằm trong repository, và thay đổi có thể đi theo cả hai hướng:

  • Sửa YAML trong editor rồi push lên Git, Apidog nhận thay đổi.
  • Sửa trong Apidog, thay đổi trở thành commit để team review.

Nhờ đó, contract không bị tách thành hai bản khác nhau. Nếu muốn xem chi tiết hơn về cách tiếp cận này, bạn có thể đọc thêm bài spec-first vs design-first trong Apidog.

Checklist triển khai API “spec-first”

Trước khi cho team bắt đầu build, hãy kiểm tra spec theo danh sách sau:

  • [ ] Spec hợp lệ theo OpenAPI schema.
  • [ ] Mọi endpoint có ít nhất một response thành công.
  • [ ] Mọi endpoint có ít nhất một response lỗi.
  • [ ] Schema dùng chung được đặt trong components/schemas.
  • [ ] Các schema dùng chung được tham chiếu bằng $ref, không copy thủ công.
  • [ ] Field bắt buộc được khai báo trong required.
  • [ ] Các field có format rõ ràng như uuid, email, date-time.
  • [ ] Query parameter có default, minimum hoặc maximum khi cần.
  • [ ] Request body có schema rõ ràng.
  • [ ] Status code được khai báo đúng với hành vi API.
  • [ ] Spec được commit vào Git.
  • [ ] Thay đổi spec được review qua pull request.
  • [ ] Mock server chạy từ spec.
  • [ ] Frontend có thể gọi mock server.
  • [ ] Contract test kiểm tra response thật so với schema.
  • [ ] API docs được sinh từ cùng file spec.

Nếu các mục trên đều được đáp ứng, team có thể build song song dựa trên một contract rõ ràng.

Câu hỏi thường gặp

Phát triển API “spec-first” có giống “design-first” không?

Phần lớn là có.

“Design-first”, “contract-first” và “spec-first” đều nói về cùng một nguyên tắc: định nghĩa giao diện API trước khi triển khai.

Khác biệt chủ yếu nằm ở cách gọi:

  • “Design-first” nhấn mạnh quá trình thiết kế
  • “Contract-first” nhấn mạnh thỏa thuận giữa client và server
  • “Spec-first” nhấn mạnh file OpenAPI spec là điểm bắt đầu cụ thể

Trong thực tế, các thuật ngữ này thường được dùng thay thế cho nhau.

Tôi có phải tự viết YAML không?

Không bắt buộc.

Bạn có thể:

  • Viết YAML trực tiếp
  • Dùng visual editor để tạo spec
  • Import spec từ file có sẵn
  • Kết hợp visual editing với chỉnh sửa YAML khi review

Điểm quan trọng không phải là bạn gõ YAML bằng tay hay không. Điều quan trọng là contract tồn tại, được review và được thống nhất trước khi implementation bắt đầu.

Làm thế nào để tránh spec và code bị lệch nhau?

Có ba việc nên làm:

  1. Đặt spec trong Git

    Mọi thay đổi contract phải đi qua pull request.

  2. Chạy contract test trong CI

    Nếu response thật không khớp schema, build phải fail.

  3. Sinh mock và docs từ cùng spec

    Không duy trì nhiều bản mô tả API riêng biệt.

Khi spec là nguồn sự thật duy nhất, drift giữa tài liệu, mock, test và code sẽ giảm đáng kể.

Khi nào không nên dùng “spec-first”?

Nếu bạn đang thử nghiệm một prototype rất nhỏ, chỉ dùng nội bộ và API có thể bị xóa ngay sau đó, spec-first có thể là overhead.

Nhưng khi API có nhiều consumer, nhiều team cùng làm việc, hoặc cần tài liệu và kiểm thử ổn định, spec-first thường tiết kiệm thời gian hơn so với việc sửa lỗi tích hợp về sau.

Kết luận

Phát triển API “spec-first” chỉ thay đổi thứ tự làm việc: viết contract trước, thống nhất contract, rồi mới triển khai. Nhưng thay đổi nhỏ này giúp frontend, backend và QA làm việc song song với ít phỏng đoán hơn.

Nếu muốn thử workflow đầy đủ, hãy mở Chế độ Spec-First trong Apidog và trỏ nó đến repository chứa OpenAPI spec của bạn.

Top comments (0)