iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Software Development

收到自己的紅色炸彈!婚期就是 Deadline:前端一人用 AI 規格流水線,30 天出貨婚禮 SaaS系列 第 10

Day 10 先立合約,再蓋房子:打造 SSOT 型別合約防線,連 Server Handler 都不放過

  • 分享至 

  • xImage
  •  

❯❯ feature-to-api:合約型別先行,讓編譯器提早翻臉
Day 10 先立合約,再蓋房子

📍 流水線位置|入口 → flow → 【型別】 → mock → 測試 → UI → vibe → 上線

流水線位置|入口 → flow → 【型別】 → mock → 測試 → UI → vibe → 上線


前九天都在搞規格,從需求怎麼進來(D5–7)、規格錯了怎麼修改(D8)、發現程式碼跑偏怎麼辦(D9),系列走完三分之一,流水線前兩站(入口、flow)已全部順完,規格層算是徹底立穩了。從今天起的十天,要把這疊紙上的規格變成能跑的系統。開工第一件事,我們先來動型別。

為什麼第一站是型別?因為要先封殺前後端協作中最經典的翻車劇本:
後端偷偷改了欄位名稱忘了通知;或是通知了,前端卻漏改。開發環境看不出來,測試環境也沒攔截到,直到正式站某個頁面突然一片空白,大家才發現 Request 裡帶的依然是舊欄位。

「傳統口頭約定 vs. 合約型別先行」除錯成本對照圖

這類事故的根源,是前後端之間缺一份具備強制力的合約。口頭約定沒有強制力,文件也沒有強制力,能在改錯的當下立刻翻臉的,只有編譯器。所以動工蓋任何 UI 之前:先建立系統的型別合約,再進入開發。


型別由規格自動衍生

feature-to-api 讀完 flow 文件後,會在 app/types/api/ 目錄下按業務模組生成對應的型別檔。以接待帳號模組為例:

// 接待帳號:建立 / 編輯 / 移除

export interface ReceptionAccountListItem {
  accountId: string
  weddingId: string
  username: string
}

export interface CreateReceptionAccountBody {
  username: string
  // 選填:設定後此帳號可登入接待端(僅名單管理可不填)
  password?: string
}

這個檔案有三個設計:

  1. 介面(Interface)與實作分離:
    這裡只定義資料長什麼樣,不含任何業務邏輯實作。mock server 要遵守它,未來的正式後端端點也要遵守它,這就是「合約」兩個字的意思。

  2. 業務約束直接釘在型別旁:
    例如「僅名單管理可不填」這行註解,直接把欄位設計的業務背景寫死在型別旁。接手的工程師不用再去翻古董需求文件,也能秒懂設計原委。

  3. 領域名詞全線貫穿:
    flow 文件裡的「接待帳號」,對應的事件就叫 ReceptionAccountCreatedEvent。從需求規格、型別定義到自動化測試,全程採用同一套領域語言(Ubiquitous Language)。


單一真理來源:嚴禁頁面「私設型別」

為避免防線流於形式,流水線針對頁面實作立下了嚴格約束:動工前,自動化程序會先檢查 server/api/ 的實際端點,確認回應格式與參數。若發現落差,一律以合約為準修復 server,絕不反向遷就實作。

同時,頁面元件與 client 封裝層只能引用 app/types/api/ 裡的合約型別,嚴禁在頁面層私自宣告 Local Interface。若 Client 簽名與合約不符,改簽名配合合約,而不是自己開小路。

原因很簡單:系統裡只能有一份真理來源。 只要允許頁面自訂型別,合約就會出現分岔,靜態檢查便無法捕捉欄位不匹配的落差,整條防線等於形同虛設。

同理,流水線也全面禁用 globalThis.$fetch 這類繞過型別檢查的非型別安全寫法。只要留了一個缺口,安全防線就會全面潰堤。


合約的工程價值:靜態檢查與錯誤前置

這份合約立好之後,到底能幫我們省下多少除錯成本?我親自做了一次實驗。

我把 ReceptionAccountListItemusername 改名成 displayName,模擬後端偷偷改欄位的情境,存檔後直接執行 Typecheck。結果編譯器立刻噴出警告:

app/pages/weddings/[weddingId]/accounts.vue(95,32): error TS2339: Property 'username' does not exist on type 'ReceptionAccountListItem'.
server/api/v1/weddings/[weddingId]/reception-accounts/index.get.ts(14,3): error TS2322: Type '{ accountId: string; weddingId: string; username: string; }[]' is not assignable to type 'ReceptionAccountListItem[]'.
  Property 'displayName' is missing in type '{ accountId: string; weddingId: string; username: string; }' but required in type 'ReceptionAccountListItem'.

這次改動讓accounts.vue瞬間跳出 7 處型別錯誤(第 95、193、198、202、218、229、430 行),每一個試圖讀取 username 的位置全部被精準點名!

更精彩的是第二段:連 server 端的 handler 也被抓了,理由是反向的:「你回傳的資料缺少了 displayName」。一份 Interface,同時雙向鎖住了前端與後端:前端不准讀取合約外的欄位,後端不准回傳不符合約的資料。 隨便改動一個欄位名,全系統所有受影響的角落,編譯器在一秒之內全部為你攤開。

「合約 Interface 雙向管住 前端 與 後端」防禦矩陣圖

在沒有合約保護的世界裡,這些錯誤都會被延後到「執行期(Runtime)」才爆發。開發與測試階段未必能第一時間察覺,直到某位使用者在正式站看到空白頁面時,問題才徹底暴露。兩者的差別,僅僅在於翻臉的時間點:在本地開發時被編譯器罵,修復成本只需要一點時間;在正式站被使用者當場抓包,那就是災難級的系統事故。

合約立好了,房子怎麼蓋?下一步就是解封前端開發生產力。
明天來聊聊型別合約的第一個實戰應用:與後端開發完全解耦的 Mock Server。

🎒 最小一步| 梳理專案中曾發生欄位不一致的 API,將其請求與回應欄位定義為標準 Interface 並集中存放於共用型別目錄,作為系統合約之基礎。
📎 本篇證據| app/types/api/accounts.ts(包含欄位定義與業務約束註解之原始片段)


上一篇
Day 09 別再相信「我以為我記得」!規格跟程式碼偷偷分岔,我怎麼抓?
下一篇
Day 11 後端還沒好,但我一天都沒等
系列文
收到自己的紅色炸彈!婚期就是 Deadline:前端一人用 AI 規格流水線,30 天出貨婚禮 SaaS14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言