團隊協作常見問題:契約、配置與夾具
結論:命名風格、時間格式、空值語義、列舉形態、配置格式、夾具組織與合併衝突,十二條團隊裡真正會吵起來的取捨。

結論:一次性把這幾條寫進團隊約定——返回體用 {"data":…,"meta":…} 加 HTTP 狀態碼、命名全站統一同一種風格、時間用帶時區的 RFC 3339 字串、列舉用字串;之後所有新介面都按同一份契約「只加不改不刪」地演進。
這些問題沒有唯一正確答案,但必須有唯一團隊答案——否則每次新介面都要重新吵一遍。
一、介面契約
介面返回的 JSON 該用什麼結構?
三種常見形態,各有代價:
| 形態 | 例子 | 優點 | 缺點 |
|---|---|---|---|
| 裸資料 | [{...}, {...}] |
簡單 | 無法擴充套件元資訊 |
| 固定包裝 | {"code":0,"message":"ok","data":{...}} |
統一錯誤處理 | 業務碼與 HTTP 狀態重複 |
| 資源包裝 | {"data":{...},"meta":{...},"links":{...}} |
可擴充套件、語義清晰 | 層級多一層 |
對內的業務 API 推薦「資源包裝 + HTTP 狀態碼錶達結果」:
{
"data": [{ "id": "1", "name": "示例" }],
"meta": { "total": 128, "page": 2, "pageSize": 20 }
}
錯誤時用 HTTP 狀態碼 + 結構化錯誤體,不要再套一層 code:
{
"error": {
"type": "validation_failed",
"message": "請求引數校驗失敗",
"details": [
{ "path": "/email", "reason": "格式不正確" }
]
}
}
關鍵點:HTTP 狀態碼已經表達了成功或失敗,業務碼是冗餘的。兩套體系並存時, 「201 但 code 是 500」這種不一致幾乎必然出現。
裸陣列要避免。頂層是陣列時無法附加分頁、總數、遊標,將來想加就得改所有呼叫方。
欄位命名用 snake_case 還是 camelCase?
選一個,全站統一,不要在同一份 JSON 裡混用。
| 風格 | 常見生態 | 例子 |
|---|---|---|
camelCase |
JavaScript / TypeScript / Java | userName, createdAt |
snake_case |
Python / Ruby / Go(部分) / 資料庫列名 | user_name, created_at |
kebab-case |
罕見,鍵名裡帶連字元訪問不便 | user-name |
判斷依據:
- 誰消費這份 JSON:前端為主選 camelCase,資料管道與 Python 服務為主選 snake_case;
- 是否直接對映資料庫列:直接透傳表結構時用 snake_case 能省一層轉換;
- 存量系統延續:已有介面是什麼就用什麼,一致性比正確性重要。
真要轉換,放在邊界做(閘道器或前端請求層),不要在業務程式碼裡散落 toCamelCase 呼叫。
時間欄位用什麼格式?
用 RFC 3339 / ISO 8601 字串,帶時區偏移:
{
"createdAt": "2026-10-30T11:37:57+08:00",
"updatedAt": "2026-10-30T03:37:57Z"
}
要點:
- 帶時區:不帶時區的
2026-10-30 11:37:57在跨時區場景下必然出問題; - 統一
Z或帶偏移:兩者都對,但一個介面裡別混用; - 不用時間戳數字:秒還是毫秒、是否含閏秒,每個團隊都要重新解釋一遍。需要緊湊時用毫秒整數, 但要在文件裡寫死單位。
欄位名建議用 createdAt / updatedAt / deletedAt 這類 At 字尾,
一眼能看出是時間點而不是時長。時長用 timeoutMs、ttlSeconds,單位寫進名字。
空值用 null 還是省略欄位?
取決於「這個欄位有沒有意義」:
| 語義 | 寫法 | 說明 |
|---|---|---|
| 欄位存在但暫無值 | "name": null |
明確表達「查過了,就是空」 |
| 欄位與該資源無關 | 省略 | 例如普通使用者不該有 vipExpireAt |
| 部分更新(PATCH) | 省略 = 不改 | 這時 null 才表示「清空」 |
第三種最容易被忽略。在 PATCH 請求裡:
{ "nickname": null }
意思是「把暱稱清空」,而請求體裡不出現 nickname 表示「不要動暱稱」。
如果兩者都當「清空」處理,使用者改個郵箱就會把暱稱清掉。
再強調一次:不要用 undefined。它不是合法 JSON 值,JSON.stringify 會直接丟掉這個鍵,
行為與「省略」相同,但程式碼裡看起來像「顯式傳了空」。
布林欄位怎麼命名?
用 is / has / can / should 字首,避免用否定式。
{
"isActive": true,
"hasPassword": false,
"canEdit": true
}
三條經驗:
- 不要否定式:
isDisabled: false需要讀者做一次取反,isEnabled: true更直接。 雙重否定(isNotDisabled)是災難; - 不要用數字表示布林:
"enabled": 1會讓每個消費方都寫一次=== 1; - 不要用字串:
"enable": "true"在 JavaScript 裡是 truthy,if (data.enable)對"false"也成立,這類 bug 極難排查。
列舉欄位用數字還是字串?
用字串,除非有明確的儲存或頻寬壓力。
{ "status": "active" }
對比 { "status": 1 },字串的優勢:
- 日誌、抓包、資料庫裡直接可讀,不用查對映表;
- 新增列舉值不影響已有值的含義(數字列舉中間插入一個值就會錯位);
- 前端不需要同步維護一份
1 → 'active'的對映。
代價是多幾個位元組,壓縮後幾乎可忽略。
配套做法:字串列舉值用小寫 + 下劃線(pending_review),並在 Schema 裡用 enum 限定,
讓非法值在校驗階段就被攔住。
二、配置與夾具
配置用 JSON 還是 YAML / TOML?
按用途選,不要按喜好選:
| 格式 | 適合 | 不適合 |
|---|---|---|
| JSON | 程式讀寫、需要嚴格結構、要過 Schema 校驗 | 人工頻繁編輯(不能寫註釋) |
| YAML | 多層巢狀的部署配置(K8s) | 手寫容易縮排出錯;yes/no 會被轉成布林 |
| TOML | 扁平的依賴與工具配置(Cargo.toml、pyproject.toml) |
深層巢狀可讀性差 |
| JSONC | 需要註釋的編輯器配置(tsconfig.json) |
跨系統傳輸 |
團隊裡常見的錯誤是用 YAML 存業務配置——YAML 的隱式型別轉換(1.0 變數字、
NO 變布林、2026-10-30 變日期)會在不同解析器上產生不同結果。
如果配置要給程式做嚴格校驗,選 JSON + Schema,註釋用 _comment 欄位或獨立文件補充。
測試夾具怎麼組織?
按「一份基準 + 最小差異」組織,不要每個用例複製一整份巨大 JSON。
fixtures/
├─ users/
│ ├─ base.json # 基準使用者物件
│ ├─ no-email.json # 只有 email 缺失
│ └─ invalid-age.json # 只有 age 越界
└─ fixtures.ts # 載入與淺合併的工具函式
import baseUser from './users/base.json';
export function makeUser(overrides: Partial<User> = {}): User {
return { ...baseUser, ...overrides };
}
這樣新增用例時只需要寫差異欄位,改基準欄位時所有用例自動跟上。
三條紀律:
- 夾具要能被 Schema 校驗:基準夾具本身就應該是合法資料;
- 不要從生產庫匯出真實資料:手機號、身份證、地址都是個人資訊;
- 夾具進版本庫,但要小:單檔案超過幾十 KB 就該考慮裁剪或生成。
JSON 檔案合併衝突怎麼減少?
JSON 天然容易衝突:格式化工具一次重排整個檔案,兩個人各改一行就會全檔案衝突。
四個手段:
- 統一格式化並加進
.editorconfig:縮排、換行、末尾換行一致,避免無關 diff; - 排序鍵名:約定按字母序排列鍵,新增欄位不會和別人的插入位置打架;
- 拆分檔案:一個巨型
config.json拆成按領域分的多個檔案,衝突面自然變小; - 避免「一鍵重排」提交:格式化與業務改動分成兩個提交,review 時才看得清。
如果團隊用鎖檔案(pnpm-lock.yaml、package-lock.json),衝突時不要手工合併,
重新生成即可:
git checkout --theirs pnpm-lock.yaml
pnpm install
怎麼做版本相容?
原則是「只加不改不刪」。
| 變更 | 相容性 | 做法 |
|---|---|---|
| 新增可選欄位 | 相容 | 直接加,消費方忽略未知欄位 |
| 新增必填欄位 | 不相容 | 升版本,或給預設值 |
| 刪除欄位 | 不相容 | 先標記廢棄,觀察呼叫方後再刪 |
| 改欄位型別 | 不相容 | 新增欄位名,雙寫過渡 |
| 改列舉值集合 | 視情況 | 新增值相容,刪值不相容 |
配套機制:
- 消費方用「寬鬆解析」:忽略未知欄位,欄位缺失時給預設值;
- 服務端保留舊欄位一段時間,並在響應裡返回
Deprecation頭提示; - Schema 裡對新增欄位不設
required,對廢棄欄位標deprecated: true。
怎麼在 CI 裡校驗契約?
把 Schema 當成程式碼的一部分,提交時校驗。
# 校驗示例資料是否符合 Schema(用 ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false
CI 裡跑三件事:
- Schema 自身的合法性:
$schema宣告是否有效、$ref能否解析; - 夾具是否符合 Schema:保證測試資料不會漂移;
- 契約是否向後相容:對比上一個版本,檢查是否刪了欄位、改了型別。
第三項需要工具支援(如 openapi-diff、json-schema-diff),
沒有的話至少用指令碼對比 required 陣列與 properties 鍵集合的變化。
三、速查
| 問題 | 建議 |
|---|---|
| 返回結構 | {"data":..., "meta":...},錯誤用 HTTP 狀態碼 |
| 命名風格 | camelCase(前端為主)或 snake_case(資料為主),全站統一 |
| 時間格式 | RFC 3339 字串,帶時區,欄位名以 At 結尾 |
| 空值 | 無值用 null,無關欄位省略,PATCH 裡省略表示不改 |
| 布林命名 | is/has/can 字首,不用否定式,不用 0/1 |
| 列舉 | 字串,小寫下劃線,Schema 里加 enum |
| 配置格式 | 程式校驗用 JSON,人工編輯用 TOML 或 JSONC |
| 夾具 | 一份基準 + 最小差異覆蓋,能被 Schema 校驗 |
| 合併衝突 | 統一格式化、排序鍵名、拆分檔案 |
| 版本相容 | 只加不改不刪,廢棄欄位先標記再刪 |