Lock-in 有幾種層次

把「被框架綁住」這件事分開來看,鎖定其實有不同的層次:

第一層:API 語法層

Express 的 (req, res, next) → NestJS 的 @Controller / @Get / @Body()。這層是最表面的 lock-in,改起來機械性很高——每個 route handler 都要改,但改法都一樣,技術上可以用 script 輔助。

第二層:生態選型層

你用了 express-session、express-rate-limit、express-validator。這些套件的 API 假設你在用 Express,換框架就要找對應版本或自己包裝 adapter。有的有一對一替代(rate limit 每個框架都有),有的沒有(某些 express-specific middleware 要重寫)。

第三層:架構思維層

這是最難的。如果你的 Express 系統已經有分層(controller / service / repository),遷 NestJS 相對容易——NestJS 的 Module 對應你的 service 邊界,Injectable 對應你的 service class。但如果你的 Express 系統是「所有邏輯在 route handler 裡」,遷 NestJS 等於同時重構架構和換框架,兩個困難的工作同時做。


遷移的真實成本

以一個 5 萬行的 Express + TypeScript 系統遷移到 NestJS 為例:

機械性工作(估計 30% 的工)

  • app.get('/path', handler) 換成 @Controller / @Get
  • express-validator 換成 class-validator + ValidationPipe
  • 把手動的 require('service') 換成 DI constructor 注入

架構工作(估計 50% 的工)

  • 把散落在 route 裡的邏輯提取到 Service 層
  • 把重複的 middleware 邏輯重組成 Guard / Interceptor / Pipe
  • 把隱式的模組邊界用 NestJS Module 明確化

測試工作(估計 20% 的工)

  • 重寫 supertest 的 integration test(因為 NestJS 的 server 建立方式不同)
  • 更新 mock 方式(DI 的 mock 方式和手動 require 不同)

最大的隱性成本認知切換。工程師要在還要繼續交付功能的同時,學習 NestJS 的 Module / Injectable / Guard 體系。知識不足的情況下做遷移,很容易做出「外觀是 NestJS 但本質還是 Express」的結果——@Injectable() 標了但還是在每個 service 裡 new OtherService()


Layered Architecture 如何降低 Lock-in

最有效的降低 framework lock-in 的方法是:讓業務邏輯不知道 framework 的存在

// ❌ 高度 lock-in:Service 依賴 Express 型別
class UserService {
  async create(req: express.Request) {
    const { name, email } = req.body; // ← 直接依賴 Express 的 Request
    return db.create({ name, email });
  }
}
// ✅ 低 lock-in:Service 只接 plain DTO
class UserService {
  async create(dto: CreateUserDto) { // ← 不知道 Express 的存在
    return db.create(dto);
  }
}
 
// Controller(薄層,負責 framework 轉換)
class UserController {
  async store(req: Request, res: Response) {
    const dto = { name: req.body.name, email: req.body.email }; // ← 轉換在 Controller
    const user = await userService.create(dto);
    res.json(user);
  }
}

如果你的 Service 層不知道 express.Request 存在,遷移時只需要改 Controller 層——Service 層不用動。

這正是 proto 的設計理念:app/services/ 裡的 class 不 import 任何 Express 型別,只接 DTO 和回傳 plain object。


什麼情況 Lock-in 代價值得付

不是每種 lock-in 都值得花力氣消除,要問:換掉的可能性有多高?換掉的收益有多大?

高可能性換掉 + 收益大:投資降低 lock-in(例如:ORM 選一個有 repository pattern 抽象的,讓底層 DB 可以換)。

低可能性換掉:接受 lock-in,不要過度抽象。如果你用 PostgreSQL 五年從來沒考慮過換,把所有 DB 操作包在抽象層後面只是增加複雜度。

Framework lock-in 的現實:換框架在大部分系統裡是低優先事項,因為框架只是你系統的外殼,業務邏輯才是核心價值。投資讓業務邏輯可測、可複用比投資讓它可換框架更有實際收益。


如果你真的要換

漸進式遷移比全部重寫風險低:

  1. 先建立新的 Service / Repository 層(不帶 framework 依賴的 business logic)
  2. 舊的 route handler 逐步委托給新的 Service(Controller 層薄化)
  3. 新的功能用新框架開發(不要在舊框架上繼續疊)
  4. 舊的 route 逐批遷移(每批上線後觀察一段時間再繼續)

這比「某週一全部切換」的風險小得多,而且每個階段都可以停下來評估:「繼續遷移的收益還值得投入嗎?」


延伸閱讀