Câu hỏi thường gặp về cộng tác nhóm: hợp đồng, cấu hình và fixture
Kết luận: Phong cách đặt tên, định dạng thời gian, ngữ nghĩa null, hình dạng enum, định dạng cấu hình, tổ chức fixture và xung đột merge — mười hai điểm đánh đổi mà các nhóm thực sự tranh cãi.

Tóm lại: hãy chốt những điều này một lần trong quy ước của nhóm — envelope {"data":…,"meta":…} kèm mã trạng thái HTTP, một phong cách đặt tên cho toàn bộ hệ thống, timestamp RFC 3339 có múi giờ, và enum dạng chuỗi — rồi phát triển mọi API mới theo cùng hợp đồng đó bằng cách chỉ thêm trường, không bao giờ đổi hay xóa chúng.
Những câu hỏi này không có câu trả lời đúng duy nhất, nhưng một nhóm phải có một câu trả lời duy nhất — nếu không, mỗi API mới lại khơi lại cuộc tranh luận cũ.
1. Hợp đồng API
Một API nên trả về cấu trúc nào?
Ba dạng phổ biến, mỗi dạng có một cái giá:
| Dạng | Ví dụ | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Dữ liệu thô | [{...}, {...}] |
Đơn giản | Không thể mang metadata |
| Envelope cố định | {"code":0,"message":"ok","data":{...}} |
Xử lý lỗi thống nhất | Mã nghiệp vụ trùng lặp với mã trạng thái HTTP |
| Envelope tài nguyên | {"data":{...},"meta":{...},"links":{...}} |
Mở rộng được, ngữ nghĩa rõ | Thêm một cấp lồng nhau |
Với API nghiệp vụ nội bộ, hãy ưu tiên "envelope tài nguyên + mã trạng thái HTTP thể hiện kết quả":
{
"data": [{ "id": "1", "name": "示例" }],
"meta": { "total": 128, "page": 2, "pageSize": 20 }
}
Với lỗi, hãy dùng mã trạng thái HTTP kèm phần thân lỗi có cấu trúc thay vì lồng thêm một code khác:
{
"error": {
"type": "validation_failed",
"message": "请求参数校验失败",
"details": [
{ "path": "/email", "reason": "格式不正确" }
]
}
}
Điểm mấu chốt: mã trạng thái HTTP đã thể hiện thành công hay thất bại, nên mã nghiệp vụ là thừa. Khi hai hệ thống cùng tồn tại, những mâu thuẫn như "201 nhưng code là 500" gần như không thể tránh khỏi.
Tránh mảng trần. Với mảng ở cấp cao nhất, bạn không thể gắn phân trang, tổng số hay con trỏ, và thêm chúng sau này nghĩa là sửa mọi phía gọi.
Tên trường nên dùng snake_case hay camelCase?
Chọn một, áp dụng cho toàn bộ hệ thống, và không bao giờ trộn chúng trong cùng một tài liệu JSON.
| Phong cách | Hệ sinh thái phổ biến | Ví dụ |
|---|---|---|
camelCase |
JavaScript / TypeScript / Java | userName, createdAt |
snake_case |
Python / Ruby / Go (một phần) / cột cơ sở dữ liệu | user_name, created_at |
kebab-case |
Hiếm; khóa có gạch nối rất khó truy cập | user-name |
Tiêu chí:
- Ai tiêu thụ JSON này: chọn camelCase khi frontend chiếm ưu thế, snake_case khi đường ống dữ liệu và dịch vụ Python chiếm ưu thế;
- Nó có ánh xạ trực tiếp sang cột cơ sở dữ liệu không: snake_case tiết kiệm một tầng chuyển đổi khi truyền thẳng cấu trúc bảng;
- Tính liên tục của hệ thống hiện có: giữ nguyên những gì API hiện tại đang dùng; tính nhất quán quan trọng hơn tính đúng đắn.
Nếu thực sự cần chuyển đổi, hãy làm ở ranh giới (gateway hoặc tầng yêu cầu của frontend) thay vì rải các lời gọi toCamelCase khắp mã nghiệp vụ.
Trường thời gian nên dùng định dạng nào?
Dùng chuỗi RFC 3339 / ISO 8601 có offset múi giờ:
{
"createdAt": "2026-10-30T11:37:57+08:00",
"updatedAt": "2026-10-30T03:37:57Z"
}
Những điểm chính:
- Kèm múi giờ:
2026-10-30 11:37:57không có múi giờ sẽ chắc chắn hỏng trong các tình huống xuyên múi giờ; - Dùng
Zhoặc offset một cách nhất quán: cả hai đều đúng, nhưng đừng trộn chúng trong cùng một API; - Tránh timestamp dạng số: dù là giây hay mili giây, và có tính giây nhuận hay không, mỗi nhóm đều phải diễn giải lại. Khi cần gọn, hãy dùng số nguyên mili giây, nhưng chốt đơn vị trong tài liệu.
Ưu tiên tên trường có hậu tố At như createdAt / updatedAt / deletedAt,
để chỉ nhìn là biết giá trị là một điểm thời gian chứ không phải một khoảng thời lượng. Với thời lượng, dùng timeoutMs, ttlSeconds — đặt đơn vị vào tên.
Null nên là null hay bỏ trường đi?
Tùy vào việc "trường này có ý nghĩa" hay không:
| Ngữ nghĩa | Dạng | Ghi chú |
|---|---|---|
| Trường tồn tại nhưng chưa có giá trị | "name": null |
Nói rõ "đã kiểm tra và nó rỗng" |
| Trường không liên quan đến tài nguyên này | Bỏ đi | Ví dụ, người dùng thường không nên có vipExpireAt |
| Cập nhật một phần (PATCH) | Bỏ đi = không thay đổi | Chỉ null mới nghĩa là "xóa" ở đây |
Trường hợp thứ ba là chỗ hay bị bỏ qua nhất. Trong một yêu cầu PATCH:
{ "nickname": null }
nghĩa là "xóa biệt danh", trong khi bỏ nickname khỏi phần thân yêu cầu nghĩa là "không đụng đến biệt danh".
Nếu bạn coi cả hai là "xóa", một người dùng đổi email sẽ xóa luôn biệt danh của mình.
Một lần nữa: đừng dùng undefined. Nó không phải giá trị JSON hợp lệ, và JSON.stringify bỏ hẳn khóa đó,
nên hành vi khớp với "bỏ đi" trong khi mã trông như "đã truyền giá trị rỗng một cách rõ ràng".
Trường boolean nên đặt tên thế nào?
Dùng tiền tố is / has / can / should và tránh phủ định.
{
"isActive": true,
"hasPassword": false,
"canEdit": true
}
Ba bài học:
- Không phủ định:
isDisabled: falsebuộc người đọc phải đảo nghĩa một lần;isEnabled: truetrực tiếp hơn. Phủ định kép (isNotDisabled) là thảm họa; - Đừng dùng số cho boolean:
"enabled": 1buộc mọi phía tiêu thụ phải viết=== 1một lần; - Đừng dùng chuỗi:
"enable": "true"là truthy trong JavaScript, vàif (data.enable)cũng đúng với"false"; những lỗi như vậy cực khó truy vết.
Trường enum nên dùng số hay chuỗi?
Dùng chuỗi trừ khi có ràng buộc rõ ràng về lưu trữ hoặc băng thông.
{ "status": "active" }
So với { "status": 1 }, chuỗi thắng vì:
- Log, bản bắt gói và cơ sở dữ liệu đọc được trực tiếp mà không cần tra bảng ánh xạ;
- Thêm một giá trị enum không thay đổi ý nghĩa của các giá trị hiện có (chèn một giá trị vào giữa enum dạng số sẽ làm lệch mọi thứ);
- Frontend không cần duy trì ánh xạ
1 → 'active'đồng bộ.
Cái giá là vài byte thừa, gần như không đáng kể sau khi nén.
Thực hành đi kèm: dùng chữ thường + gạch dưới cho giá trị enum dạng chuỗi (pending_review), và ràng buộc chúng bằng enum trong Schema
để giá trị không hợp lệ bị từ chối ngay ở giai đoạn kiểm chứng.
2. Cấu hình và fixture
Cấu hình nên là JSON, YAML hay TOML?
Chọn theo mục đích, không theo sở thích:
| Định dạng | Tốt cho | Không tốt cho |
|---|---|---|
| JSON | Đọc ghi bằng chương trình, dữ liệu có cấu trúc chặt, kiểm chứng bằng Schema | Chỉnh tay thường xuyên (không cho phép chú thích) |
| YAML | Cấu hình triển khai lồng sâu (K8s) | Thụt lề viết tay dễ lỗi; yes/no biến thành boolean |
| TOML | Cấu hình phẳng cho phụ thuộc và công cụ (Cargo.toml, pyproject.toml) |
Độ dễ đọc kém với lồng sâu |
| JSONC | Cấu hình trình soạn thảo cần chú thích (tsconfig.json) |
Đi qua ranh giới hệ thống |
Một lỗi phổ biến trong các nhóm là lưu cấu hình nghiệp vụ trong YAML — việc chuyển kiểu ngầm của YAML (1.0 thành số,
NO thành boolean, 2026-10-30 thành ngày) cho kết quả khác nhau giữa các trình phân tích.
Nếu cấu hình phải được chương trình kiểm chứng nghiêm ngặt, hãy chọn JSON + Schema, và ghi chú giải thích qua trường _comment hoặc tài liệu riêng.
Fixture kiểm thử nên tổ chức thế nào?
Tổ chức theo "một bản nền + khác biệt tối thiểu", không phải sao chép một JSON khổng lồ cho mỗi ca kiểm thử.
fixtures/
├─ users/
│ ├─ base.json # Baseline user object
│ ├─ no-email.json # Only email is missing
│ └─ invalid-age.json # Only age is out of range
└─ fixtures.ts # Utility functions for loading and shallow merging
import baseUser from './users/base.json';
export function makeUser(overrides: Partial<User> = {}): User {
return { ...baseUser, ...overrides };
}
Như vậy một ca kiểm thử mới chỉ cần viết các trường khác biệt, và khi đổi một trường nền thì mọi ca tự cập nhật.
Ba quy tắc:
- Fixture phải qua kiểm chứng Schema: bản thân fixture nền phải là dữ liệu hợp lệ;
- Đừng xuất dữ liệu thật từ production: số điện thoại, số CMND và địa chỉ đều là thông tin cá nhân;
- Fixture thuộc về kiểm soát phiên bản nhưng phải nhỏ: một tệp vượt quá vài chục KB nên được cắt gọn hoặc sinh tự động.
Làm sao giảm xung đột merge trong các tệp JSON?
JSON dễ xung đột: một công cụ định dạng làm lại toàn bộ tệp cùng lúc, nên hai người mỗi người đổi một dòng lại xung đột trên cả tệp.
Bốn chiến thuật:
- Thống nhất định dạng và thêm vào
.editorconfig: thụt lề, ký tự xuống dòng và xuống dòng cuối nhất quán sẽ tránh các khác biệt không liên quan; - Sắp xếp khóa: thống nhất thứ tự chữ cái để một trường mới không đụng vị trí chèn của người khác;
- Chia tệp: tách một
config.jsonkhổng lồ thành nhiều tệp theo miền, và bề mặt xung đột tự nhiên thu hẹp; - Tránh commit "định dạng lại mọi thứ": tách việc định dạng khỏi thay đổi nghiệp vụ thành hai commit để review dễ đọc.
Nếu nhóm dùng lock file (pnpm-lock.yaml, package-lock.json), đừng merge tay; hãy sinh lại:
git checkout --theirs pnpm-lock.yaml
pnpm install
Làm sao duy trì tương thích phiên bản?
Nguyên tắc là "thêm, không đổi, không xóa".
| Thay đổi | Tương thích | Cách làm |
|---|---|---|
| Thêm trường tùy chọn | Tương thích | Thêm nó; phía tiêu thụ bỏ qua trường không biết |
| Thêm trường bắt buộc | Không tương thích | Tăng phiên bản, hoặc cung cấp giá trị mặc định |
| Xóa một trường | Không tương thích | Đánh dấu deprecated trước, theo dõi các phía gọi, rồi mới xóa |
| Đổi kiểu của một trường | Không tương thích | Thêm tên trường mới và ghi kép trong thời gian chuyển tiếp |
| Đổi tập giá trị enum | Tùy | Thêm giá trị thì tương thích; xóa thì không |
Cơ chế hỗ trợ:
- Phía tiêu thụ dùng "phân tích dễ tính": bỏ qua trường không biết và cấp giá trị mặc định cho trường thiếu;
- Máy chủ giữ trường cũ thêm một thời gian và trả header
Deprecationtrong phản hồi như gợi ý; - Trong Schema, đừng đánh dấu trường mới là
required, và gắn thẻ trường bị bỏ bằngdeprecated: true.
Làm sao kiểm chứng hợp đồng trong CI?
Hãy coi Schema là một phần của mã và kiểm chứng nó trong mỗi commit.
# Validate sample data against the Schema (using ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false
Chạy ba việc trong CI:
- Tính hợp lệ của bản thân Schema: khai báo
$schemacó hợp lệ không và$refcó phân giải được không; - Fixture có khớp Schema không: đảm bảo dữ liệu kiểm thử không trôi;
- Hợp đồng có tương thích ngược không: so với phiên bản trước và kiểm tra trường bị xóa hoặc kiểu bị đổi.
Mục thứ ba cần công cụ hỗ trợ (như openapi-diff hoặc json-schema-diff);
nếu không có, ít nhất hãy so các thay đổi trong mảng required và tập khóa properties bằng script.
3. Bảng tra nhanh
| Câu hỏi | Khuyến nghị |
|---|---|
| Cấu trúc phản hồi | {"data":..., "meta":...}, dùng mã trạng thái HTTP cho lỗi |
| Phong cách đặt tên | camelCase (ưu tiên frontend) hoặc snake_case (ưu tiên dữ liệu), nhất quán toàn hệ thống |
| Định dạng thời gian | Chuỗi RFC 3339 có múi giờ, tên trường kết thúc bằng At |
| Null | Dùng null cho không có giá trị, bỏ trường không liên quan, bỏ trong PATCH nghĩa là "không thay đổi" |
| Tên boolean | Tiền tố is/has/can, không phủ định, không 0/1 |
| Enum | Chuỗi, chữ thường có gạch dưới, thêm enum trong Schema |
| Định dạng cấu hình | JSON khi cần kiểm chứng bằng chương trình, TOML hoặc JSONC khi chỉnh tay |
| Fixture | Một bản nền + các ghi đè khác biệt tối thiểu, hợp lệ với Schema |
| Xung đột merge | Thống nhất định dạng, sắp xếp khóa, chia tệp |
| Tương thích phiên bản | Thêm, không đổi, không xóa; đánh dấu deprecated trước khi xóa |