跳至主要內容
← 返回文章
8 分鐘閱讀系統與基礎架構

作品集生態系:收斂成單一寫入路徑

作品集網站長到一定規模後,我發現「寫入資料」這件事散落在太多地方 —— 前台自己的 API 會寫、內容後台直接連 MongoDB 寫,我甚至手動用腳本塞過文章。每個入口都各自「知道」一點資料規則:文章要有唯一 slug、中文標題與內文必填、分類只能是那五種……規則重複、而且遲早會不一致。

這篇記錄我怎麼把整個生態系的寫入,收斂成一條經過驗證的路徑

問題:多入口等於多份規則

同一份 posts 集合,原本有三個地方會寫它。只要其中一個忘了檢查某條規則,就可能寫進一筆壞資料 —— 例如一篇沒有 slug 的文章,前台詳情頁就會 404。更麻煩的是,規則散在各處,改一條要同步好幾個檔案,這種「共識靠自律」的設計,規模一大就會崩。

我要的是:不管哪個 client 來寫,都只有一道關卡在驗證,而且規則只寫一次。

決定:開一支中央 API 服務層

於是我開了第三支後端 repo tkflyc-webservice,一支專門用來「串服務」的中央 API,用 NestJS + 標準的 DDD / Clean Architecture。我的原則是「網域名即定位、高內聚低耦合」,所以它有自己的網域、自己的職責:所有寫入與查詢都集中經過它。

分層很單純,依賴一律由外向內:

domain/          Post 聚合根 + 領域不變式(規則的唯一真相)、Repository 介面(port)
application/     CQRS command / query + handler、對外部相依的 port
infrastructure/  MongoDB 實作、revalidate 呼叫(填 port)
interfaces/http/ 認證、例外轉換、兩個對外介面

規則被關進 domain 層的 Post 聚合根。它有一個 checkInvariants,是整個系統對「什麼是合法文章」的唯一定義:

article / devlog 需要中文標題與內文;article 需要唯一 slug;
publishedAt 必須是合法日期。違反就丟 DomainError → HTTP 400。

infrastructure 用「ports and adapters」接上去:domain 只認得抽象的 PostRepository,真正連 Mongo 的實作在 infrastructure 注入進來。要換資料庫、要寫測試,domain 完全不用動。

封包分派:像遊戲伺服器讀 opcode

處理請求的方式,我刻意做成遊戲伺服器那種顯式命令分派。一個請求就是一個封包:

POST /gateway  { op, data }
        │
        ▼
   OP_TABLE[op]   ← 查表(op = 封包頭)
        │
        ▼
  build 出 Command / Query → 丟上 CQRS bus → 對應 handler 處理 data

OP_TABLE 就是那張「opcode → handler」對照表,每一列長這樣:

'blog.create': {
  kind: 'command',
  build: (d) => new WriteBlogCommand(d),
  present: (r) => toPostResponse(r),
},

要加一個新服務操作,只要在表裡多一列 + 對應的 command / handler,不用開新路由。 這種「查表分派」的好處是:入口只有一個、行為集中、擴充成本固定。我很習慣這種模式 —— 它跟遊戲伺服器讀封包、查 opcode、丟給 handler 是同一套心智模型。

一條路徑,兩個介面

同一條 CQRS bus,我對外開了兩個介面:上面的封包 gateway,以及資源型的 REST /posts。兩者最後都走同一條 bus、同一套領域驗證 —— 換句話說,介面可以有很多種,但寫入路徑只有一條

混合路徑:知道什麼時候「不要」統一

統一寫入聽起來很爽,但我留了一個刻意的例外:科技新聞(RSS 自動抓的 news)維持直接寫 Mongo。 因為 webservice 的 Post 聚合根只建模了文章 / 日誌的欄位,沒有 news 專屬的來源連結、原始標題、我的評論那些欄位 —— 硬把 news 塞進 gateway,反而會把那些欄位靜默丟掉。

這是我覺得比「無腦統一」更重要的判斷:收斂是為了消除重複的規則,不是為了消除所有差異。 news 有它自己的形狀,就讓它走自己的路。

串 client:統一入口,不是統一成「一個」client

有了 gateway,我把內容後台的文章 / 日誌寫入改成打它;新增文章的人再也不用知道 Mongo 長怎樣,格式錯了伺服器會直接回一句清楚的錯誤。

過程中我一度寫了一支發文用的 CLI,後來又把它砍了。有趣的是我當下的直覺是「這不就多開一個後門?」—— 但其實它跟其他 client 打的是同一條 gateway、同一把 token、同一套驗證,它不是另一條路,只是另一個 client。這裡有個容易混淆的區別:

「統一寫入路徑」是寫入層的概念(全部 → gateway → 驗一次),而它不等於「把 client 收斂成一個」。

CLI 沒有違反統一路徑。我砍掉它,是為了另一個理由:少一個要維護的入口、不要把長期 token 散落成各台機器上的檔案。最後對外就留兩個入口 —— 給人用的互動式 API 文件頁(Swagger),跟內容後台,兩者都走同一條 gateway。少即是多。

全部自架在一台筆電

這整套 —— 前台、中央 API、內容後台、資料庫 —— 全部自架在一台筆電上,靠 ngrok 固定網域 + Windows 工作排程器(開機自啟、掛了自動拉起)撐著。沒有雲、沒有 k8s,一台會發熱的筆電就是我的正式環境。

收斂之後

現在不管從哪裡寫一篇文章,都會經過同一道領域驗證、寫進同一個地方、再自動通知前台刷新。規則只有一份、入口數得出來、每一條寫入都可被追溯。

這篇文章本身,就是透過那條新的 gateway 發出來的。