# 用 Fastify 寫一層薄 BFF

> BFF 只是一層轉譯，不需要厚。Fastify v5 用一份 JSON Schema 同時做請求驗證與回應序列化，下游呼叫用 HMAC 簽章，業務 orchestration 一出現就該重新分層。

原文：https://sheng.page/posts/thin-bff-with-fastify/ · 發布：2025-09-14 · 作者：Sheng

BFF 這個詞聽起來很架構、很正式，但它本質上就是一層轉譯：前端要什麼結構的資料，BFF 就負責把下游打回來的東西整形成那個結構，同時擋住前端不需要知道的東西。這層不用厚，厚了只是把後端的複雜度複製一份搬過來。

## 為什麼是 Fastify

Fastify v5 在 2024 年 9 月正式發布，從那之後我就沒太多理由再看其他 Node.js HTTP 框架了。核心優勢很直接：schema 驅動的 request 驗證與 response 序列化，plugin 系統有完整的作用域隔離（scope isolation），效能在 Node.js 圈子裡原本就拿得出手。

最重要的是型別支援的尺度剛好。它整合 JSON Schema，你寫一份 schema，驗證、序列化、型別推斷同時都有，不用另外拉 zod 或手補 interface。對 BFF 這種不需要複雜業務邏輯的層來說，能省不少噪音。Express 那套連 body parse 都要自己接 middleware，2025 年已經很難說服人繼續用。

## BFF 的邊界怎麼守

這層夾在前端和下游服務之間，對前端它是「後端」，對下游它是「前端」。上游進來的 request 一律不信任，schema 驗證在這裡做。下游的 call 透過 HMAC 保護，HTTP header 帶 `X-Internal-Auth`，不需要外部 PKI，內部服務共享同一把 secret 就夠。

錯誤處理和 log 都集中在這層。下游回傳的 error 不要直接丟給前端，在這裡統一包裝、log 好 request ID，trace 才接得起來。

```ts
// routes/summary.ts
import type { FastifyPluginAsync } from 'fastify'
import { createHmac } from 'node:crypto'

const schema = {
  querystring: {
    type: 'object',
    required: ['userId'],
    properties: { userId: { type: 'string', minLength: 1 } },
  },
  response: {
    200: {
      type: 'object',
      properties: {
        userId: { type: 'string' },
        balance: { type: 'number' },
      },
    },
  },
}

const summaryRoute: FastifyPluginAsync = async (fastify) => {
  fastify.get('/summary', { schema }, async (req, reply) => {
    const { userId } = req.query as { userId: string }
    const ts = Date.now().toString()
    const sig = createHmac('sha256', process.env.INTERNAL_SECRET!)
      .update(`${ts}:${userId}`)
      .digest('hex')

    const data = await fetch(`${process.env.USER_SVC}/internal/summary?userId=${userId}`, {
      headers: { 'X-Internal-Auth': `${ts}.${sig}` },
    }).then((r) => r.json())

    return data
  })
}

export default summaryRoute
```

Schema 在這裡同時做兩件事：進來的 query 不合格直接 400，出去的 response 按 schema 序列化，省力地把不需要暴露給前端的欄位過濾掉。這是 Fastify 最省腦的地方，不用手寫 pick/omit。

## 厚度控制

BFF 越薄越好，但「薄」不是不做事，是只做這層該做的事：auth context 注入、下游 call 的 HMAC 簽章、request/response 的 schema 驗證、error 包裝與 log。業務邏輯不屬於這層，一旦開始在 BFF 裡寫「如果 A 服務回 X 就去問 B 服務」這種 orchestration，就是往 API gateway 走了，責任邊界要重新想清楚。


## 補充筆記

- Fastify v5 的 schema 驅動讓驗證與序列化合而為一，BFF 層不需要額外的 validation library。
- 下游呼叫用 HMAC 簽章保護，輕量且不依賴外部 PKI，內部服務共享 secret 即可。
- 錯誤處理和 log 集中在 BFF 層，下游 error 在這裡統一包裝，不直接丟給前端。
- BFF 的邊界是 schema 驗證、認證 context 注入、下游轉譯，業務 orchestration 一旦出現就該重新評估分層。
> 想法與技術判斷出自 Sheng，和 Claude 一起起草 · 列入 20260613 blog 翻新計劃，新漆未乾。