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 đó.
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à:
- Product/backend/frontend thống nhất API contract.
- Spec được commit vào Git.
- Frontend dùng mock server sinh từ spec để phát triển.
- Backend triển khai theo spec.
- QA hoặc CI chạy contract test để xác nhận implementation khớp với spec.
- 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.
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
limittố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
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
Ở đây:
-
id,email,createdAtlà bắt buộc -
idphải có formatuuid -
emailphải có format email -
createdAtphả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"
Điểm quan trọng:
-
limitlà integer - mặc định là
20 - tối đa là
100 - response
200là một array cácUser
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
Contract này nói rõ:
- Request body là bắt buộc
-
emaillà 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
Đế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"
}
]
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
201khớp schemaUser - Gửi body thiếu
email→ nhận400 -
emailsai format → không được tạo user - Response không được thiếu field bắt buộc như
idhoặccreatedAt
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
userIdnhư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.
Một workflow thực tế có thể như sau:
- Tạo hoặc import OpenAPI spec.
- Dán spec
/usersvào trình chỉnh sửa. - Kiểm tra lỗi schema trong editor.
- Bật mock server từ spec.
- Cho frontend gọi mock endpoint.
- Sinh tài liệu API từ cùng spec.
- Khi backend sẵn sàng, chạy request/test case dựa trên spec.
- 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);
});
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);
});
Đ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:
Đặt spec trong Git
Mọi thay đổi contract phải đi qua pull request.Chạy contract test trong CI
Nếu response thật không khớp schema, build phải fail.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)