building-backends
這是後端工作的核心技能。在撰寫任何伺服器程式碼之前請先閱讀它 — 它設定了預設值與不容妥協的規範,使每個服務都保持一致,然後再將詳細程序交給特定技能處理。如果此處的規則與本地儲存庫慣例衝突,則以儲存庫為準;如果未衝突,則遵循此處規則。
首先,在建立之前先理解
- 在新增程式碼之前先閱讀現有程式碼。 符合儲存庫的框架、錯誤模型、分層、命名及測試風格。一個「正確」但與程式碼庫對抗的模式就是錯誤的模式。引入第二種方式來做儲存庫已經能做到的事是一種缺陷,而非改進。
- 釐清合約,而非實作。 在選擇設計之前,先了解輸入、輸出、錯誤案例、誰呼叫它,以及一致性/延遲期望。大多數後端重工都是因為快速建立了錯誤的合約。
- 偏愛無聊。 選擇經過驗證、可讀、顯而易見的解決方案,而非巧妙的。後端在凌晨三點無人值守時運行;最佳化對象是那位半睡半醒但必須理解它的值班工程師,而不是為了優雅。
架構 — 規則
- 分層且依賴指向內層。
transport (http) → services (use-cases) → domain (entities/rules),以及services → repositories (I/O) → domain。領域層不匯入任何外層,也不執行任何 I/O。如果某個匯入箭頭指向錯誤的方向,則設計就是壞的。 - 處理器負責編排,服務負責決定,儲存庫負責 I/O。 處理器解析/驗證輸入、呼叫服務、將結果對應到回應 — 不含任何商業邏輯。儲存庫是唯一接觸資料庫、佇列或外部 HTTP 的地方。
- 保持領域層可在沒有基礎設施的情況下測試。 如果無法在沒有資料庫或網路的情況下測試核心邏輯,則該邏輯位於錯誤的層級。
- 一個組合根。 在單一位置建置設定、注入相依性並啟動伺服器。其他所有東西接收其相依性;沒有東西會向外建構全域單例。
- 設計為無狀態。 工作階段/暫存狀態存在於共享儲存(資料庫/快取)中,而非處理程序記憶體中,以便服務橫向擴展並能在重新啟動後存活。如果你在模組層級變數中進行快取,請停下來思考。
→ 若要建立已連接這些層級的骨架,請使用 api-service-scaffold。
API 設計 — 規則
- 明確、有版本的合約。 從第一天起就對 API 進行版本化(URL 或標頭);絕不對已發布的合約進行破壞性變更 — 在舊版旁邊新增新版,並棄用舊版。
- 在邊界驗證每個輸入,轉換成型別化值。核心永遠不會看到原始請求主體。拒絕無效輸入時回傳 422 及欄位層級的詳細資訊;永遠不要信任客戶端。
- 一個錯誤模型,對應一次。 領域程式碼拋出具型別的錯誤(
NotFoundError、ConflictError、ValidationError、UnauthorizedError…);由單一邊界對應器將它們轉換為狀態碼 +{ code, message, details? }。永遠不要洩漏堆疊追蹤或內部訊息。對於「找不到」和「不屬於你」都回傳404— 區分它們會洩漏存在資訊。 - 一致的形狀。 成功時用相同封包,錯誤時也用相同封包,一致性分頁(大型集合使用游標而非偏移量),一致性欄位大小寫。呼叫者應能一次學會你的慣例。
- 不安全操作需具冪等性。
PUT/DELETE根據定義是冪等的;使用冪等金鑰讓可重試的POST變得安全。網路會重試 — 重複向客戶收費是設計錯誤。 - 正確的狀態碼。 2xx 成功,4xx 呼叫者錯誤,5xx 你的錯誤。驗證失敗是 422,不是 200 加上錯誤主體;認證失敗是 401/403,不是 400。
資料層 — 規則
- 資料庫是真理的唯一來源;在那裡強制執行不變量。 外部鍵、唯一約束、
NOT NULL和檢查約束屬於綱要,而不僅在應用程式程式碼中 — 應用程式程式碼有錯誤和競爭條件;資料庫不會說謊。 - 交易包裹不變量,且盡可能短。 必須全有或全無的所有東西放在一個交易中;持有交易時不要執行慢速工作(外部呼叫、大量運算)。長時間交易會持有鎖並使其他所有人挨餓。
- 預期並發。 兩個請求會同時命中同一行。使用樂觀鎖定(一個版本欄位)或適當的隔離層級;永遠不要在沒有保護的情況下跨往返進行讀-修改-寫入。
- 遷移必須向後相容且可逆。 永遠不要在一個部署中改變一個欄位的含義;擴展→遷移→收縮。永遠不要在熱表上取得會阻塞表格的鎖。每個遷移都附帶一個經過測試的回滾。
- 注意 N+1。 一次獲取相關資料集合,而不是在查詢迴圈中獲取。在實際端點上分析查詢數量,而不僅僅是延遲。
- 為你實際執行的查詢建立索引 — 且僅限那些。未使用的索引只會造成寫入放大。
→ 關於遷移機制(鎖定、擴展-收縮、回填、回滾),請使用 db-migration-guardian。
安全性 — 基線(不容妥協)
- 在每個請求上,伺服器端執行認證然後授權。 從不信任客戶端關於其身份或權限的聲明。在每次資源存取時檢查所有權 — 「我有一個有效的令牌」≠「這筆記錄是我的」。
- 對每個查詢使用參數化。 永遠不要使用字串拼接的 SQL。所有可注入的介面(Shell、LDAP、模板)也一樣。
- 秘密來自環境/秘密管理器,絕不來自程式碼。 原始碼、日誌或錯誤訊息中不得出現敏感資訊。(請參閱
secrets-management技能。) - 處處實行最小權限 — 資料庫使用者、服務憑證、雲端角色範圍精確限定在服務所需的範圍。
- 對入境端點和對外呼叫設定速率限制和逾時。 一個無邊界的呼叫或無限制的端點就是一個等待發生的拒絕服務攻擊。
- 在邊界進行驗證和編碼以防止注入和 SSRF;將所有外部輸入(包括來自其他內部服務的)視為不可信任。
並發、作業與失敗
- 假設每個外部呼叫都會失敗。 設定逾時、加入具有退避 + 抖動的有界重試,以及針對不穩定相依性的斷路器。沒有逾時的重試就是一個慢速相依性如何拖垮整個服務的原因。
- 讓操作具有冪等性,以便重試(你或呼叫者的)是安全的。
- 背景工作進入佇列,而不是在請求路徑中。請求入隊;工作者處理;失敗落入死信佇列以供檢查,而不是在無聲的
catch {}中。 - 高聲失敗,優雅降級。 將錯誤暴露給遙測;當非關鍵相依性故障時,進行降級(提供過時資料、跳過增強)而不是對整個請求回傳 500。
可觀測性是「完成」的一部分,而非事後想法
一個無法在生產環境中除錯的服務不算完成。結構化日誌、RED/USE 指標以及綁定到一個關聯 ID 的追蹤應在建置時一併加入,而不是在第一次事故之後。
→ 關於儀器化細節以及基數/PII 護欄,請使用 backend-observability。
測試立場
- 透過公開表面測試行為(端點/使用案例),而非私有內部實作 — 以便測試在重構後仍然有效。
- 金字塔,而非甜筒: 許多針對領域/服務邏輯的快速單元測試,一組專注的整合測試涵蓋真實資料庫和接線,少數針對關鍵路徑的端到端測試。
- 每個錯誤修復都附帶一個回歸測試,該測試在修復前失敗,並在修復後通過。
- 確定性測試。 不依賴時鐘、網路或順序;明確植入資料;測試之間相互隔離。
任何後端任務的程序
- 閱讀周圍的程式碼;確定合約(輸入/輸出/錯誤/呼叫者/一致性)。
- 將工作置於正確的層級;保持領域層無 I/O。
- 在邊緣驗證輸入;使用單一錯誤模型;回傳正確的狀態碼。
- 將不變量放入資料庫 + 短交易;處理並發;將遷移規劃為擴展-收縮。
- 應用安全基線(授權 + 所有權、參數化查詢、秘密、逾時/限制)。
- 使外部呼叫和作業具有韌性與冪等性。
- 在建置過程中進行儀器化;在正確層級新增測試,包括針對任何錯誤的回歸測試。
- 根據相關特定技能的「完成定義」進行確認。
完成定義
- 層級受到尊重(領域層無 I/O;依賴指向內層);單一組合根。
- 每個輸入在邊界經過驗證;單一錯誤模型對應一次;正確的狀態碼;有版本的合約。
- 不變量在資料庫中強制執行;交易短;並發已處理;遷移交向相容且可逆。
- 安全基線已滿足:授權 + 所有權檢查、參數化查詢、秘密外部化、逾時/速率限制已存在。
- 外部呼叫/作業具有冪等性與韌性;背景工作不在請求路徑上。
- 可觀測性已接線(綁定關聯 ID 的日誌/指標/追蹤);測試在正確層級,皆為綠色,並包含回歸覆蓋。