
每個前端開發者都遇過這種事:API 回傳的內容技術上正確,但對你正在做的東西一點都不對。首頁需要使用者的姓名、頭像,還有最近三筆訂單。你分四次呼叫 API,在客戶端把資料拼湊起來,然後祈禱網路夠快,讓使用者不會察覺到那串載入過程。一個端點回傳 47 個欄位,而你只需要 6 個,偏偏你需要的那個欄位,名字叫做 usr_acct_disp_nm 這種東西。
這是架構問題。後端是為了通用用途,或完全是另一個客戶端而設計的。Backend for Frontend(BFF) 模式就是為了解決這個問題而存在。
這個詞誕生於 SoundCloud,並由 Sam Newman 在微服務的研究中加以整理。核心概念很簡單。你為每個客戶端打造一個專屬的伺服器端層。一個想要服務所有客戶端表面(網頁、行動裝置、電視、第三方整合)的通用 API,結果是哪一個都服務不好。
BFF 是一個負責以下工作的伺服器:
「每個客戶端表面一個 BFF」這個原則沒有妥協空間。行動 App 和網頁 App 的資料需求、效能預算、互動模式都不一樣。兩者共用一個 BFF,等於又回到打造一個誰都不為其最佳化的通用 API。
一般後端關心的是 商業邏輯與資料完整性。它套用領域規則、擁有資料庫,並以 領域 能理解的語言公開資源。它不在乎你的首頁需要的 User 形態,跟你的個人檔案頁面稍微不同。
BFF 關心的是 呈現邏輯。它把下游後端提供的東西,轉換成特定前端需要的東西。實務上代表:
這個區別很重要。後端工程師用實體和不變量的角度思考。前端工程師的框架是元件,以及一個畫面需要的資料。BFF 能流利地講第二種語言。
來看看實際上的差異。沒有 BFF 時,一個儀表板頁面可能會這樣做:
// 前端透過三個獨立呼叫組裝頁面
const [user, orders, notifications] = await Promise.all([
fetch("/api/users/me"),
fetch("/api/orders?userId=me&limit=3"),
fetch("/api/notifications?userId=me&unread=true"),
]);
const unreadCount = notifications.filter((n) => !n.read).length;
const displayName = `${user.firstName} ${user.lastName}`;有 BFF 時,前端一次呼叫就拿到確切需要的東西:
// BFF 提供專為這個頁面設計的端點
const { user, recentOrders, unreadCount } = await fetch("/bff/dashboard");彙整與轉換的邏輯都在 BFF 裡。它不會散落在前端各處,也不會在多個畫面重複。
BFF 模式誕生於微服務的世界,這其來有自。當你的後端拆成十幾個服務(使用者、訂單、庫存、通知、帳單),前端就得認識所有這些服務。光是一個小小的 UI 變更,可能就得協調三個後端團隊,才能把回應的形態稍微改一下。
BFF 透過充當前端唯一的契約來解決這件事。BFF 跟它需要的下游服務溝通。如果使用者服務改了 API,BFF 會吸收這個變更。前端完全不用在乎。
這是套用到前端/後端邊界的 防腐層(anti-corruption layer) 模式。它把前端與後端演進的頻繁變動隔離開來。
BFF 用在單體上一樣有用,只是動機不同。你不是在編排多個服務。你面對的是一個沒有為你特定 UI 需求設計的通用 API。
在這種情境下,BFF 通常是單體前方一層很薄的 轉接層(adapter layer)。它可以跟後端在同一個 codebase(作為一個模組、一組 Next.js route handler,或 monorepo 裡一個獨立的 Express 應用程式),也可以獨立成一個服務。
單體情境的關鍵規則是:讓 BFF 保持輕薄。一個開始複製單體商業邏輯的 BFF,就是反模式。它應該負責翻譯與彙整。商業規則屬於後端。
這裡正是團隊最常搞錯的地方。
BFF 應該由前端團隊擁有。 這就是整個模式的重點。
如果後端工程師擁有 BFF,它會慢慢漂回一個通用 API。審查週期變慢。前端團隊最後透過 pull request 和 JIRA ticket 來協商回應形態。這些形態要等後端團隊核准才出得去。你只是把一個問題,換成同一個問題的比較慢版本。
當前端工程師擁有 BFF,他們就擁有契約。他們需要時就重塑資料。他們不用提出申請就能新增端點。他們以前端的速度出貨。
這確實要求前端工程師寫一些伺服器端程式碼。但不多。BFF 的程式碼大多是膠水工作。呼叫這個服務、轉換那個回應、回傳一個乾淨的形態。它需要你熟悉 HTTP、認證流程和非同步 I/O。能寫複雜 React 應用程式的工程師,多半都能輕鬆勝任。
擁有權的界線就是 BFF 本身。BFF 呼叫下游服務,但絕不擁有商業邏輯。資料庫寫入、領域規則、不變量都留在後端。BFF 牢牢待在呈現層。這個區別必須主動維護。
是的,這很重要,尤其當前端工程師是主要擁有者時。
搭配 Node.js runtime 的 TypeScript 是我的預設建議。 理由很務實。
當你的組織已經有 Go 的專業,而且預期 BFF 會由 full-stack 或後端工程師維護時,Go 是合理的替代方案。Go 很擅長處理並行 fan-out,這在 BFF 為了組出單一回應而同時發出多個下游請求時很重要。
語言選擇應該跟著擁有權模式走。如果前端工程師擁有 BFF,就應該用他們已經會的語言。任何其他選擇都會製造摩擦,而摩擦最終會把擁有權推回後端團隊。
安全性是支持 BFF 最有力的論點之一,而且長期被低估。
單頁應用程式有個老問題:access token 要放在哪裡?localStorage 很方便,但會被頁面上任何 JavaScript 碰到。在 npm 套件供應鏈攻擊的時代,這是真實的風險。放記憶體比較安全,但一重新整理就沒了。HTTP-only cookie 有用,但需要伺服器端處理。
BFF 徹底解決了這個問題。瀏覽器永遠不會持有原始的 access token。
BFF 負責執行 token 交換。它從你的身分提供者收到授權碼,換成 access token 和 refresh token,把 token 存在伺服器端,然後交給瀏覽器一個 HTTP-only 的 session cookie。之後前端每次請求都會帶著這個 cookie。BFF 找到 session,把真正的 token 掛到下游請求上,再轉送出去。token 永遠不會進入 JavaScript 的領域。
// BFF 處理 OAuth 回呼——瀏覽器看不到存取令牌
app.get("/auth/callback", async (req, res) => {
const { code } = req.query;
const tokens = await identityProvider.exchangeCode(code);
// 令牌儲存在伺服器端
await sessionStore.save(req.sessionId, tokens);
res.setCookie("session", req.sessionId, {
httpOnly: true,
secure: true,
sameSite: "strict",
});
res.redirect("/dashboard");
});BFF 絕對不該變成領域規則堆積的地方。讓它專注在它被設計來做的事:讀取彙整、回應塑形、認證委派。
具體來說:
如果你的 BFF 有自己的資料庫,而且開始擁有商業規則,那你已經不是在蓋 BFF,而是在蓋第二個後端。那是完全不同的問題。
如果你用 Next.js,你已經有輕量 BFF 所需的基礎設施。那就是 Route Handlers(App Router)或 API Routes(Pages Router)。你的前端和 BFF 住在同一個專案,一起部署成單一單位。
這是中小型應用程式正確的預設選擇。代價是你不能讓 BFF 獨立於前端來擴展。對多數團隊來說,這個限制無關緊要。從這裡開始。
對較大的應用程式,常見的做法是讓一個專用的 BFF 服務,在 monorepo 裡跟前端並肩而立。兩者共用 TypeScript 型別,但獨立部署。前端透過 HTTP 打 BFF。BFF 再 fan-out 到後端服務。
這種設定在營運上比較複雜,但給你獨立的擴展和更乾淨的分離。當 BFF 的複雜度足以支撐這個營運負擔時,它就值得。這代表有多種彙整模式、快取層、連線池。
在 Kubernetes 環境中,BFF 可以部署成與前端伺服器同一個 Pod 的 sidecar 容器。前端透過 localhost 跟 BFF 溝通,沒有外部網路跳點,把延遲保持在接近零。兩者永遠一起部署,也消除了版本錯位的問題。
不要讓多個前端應用程式共用一個 BFF。一旦你這麼做,就是在蓋 API gateway。你把想解決的那個一般化問題,在更高一層重新引入了。
SoundCloud 是這個模式正式被命名的地方。他們的工程團隊正是遇到了上面描述的協調問題。行動和網頁客戶端需要不同的資料形態,而一個共用的通用 API,意味著前端和後端團隊之間無止盡的協商。拆成每個客戶端各自的 BFF,出貨速度就此恢復。
Netflix 為它支援的每個裝置都跑一層 BFF。數量很多。電視、手機、平板、遊戲主機、網頁瀏覽器的效能預算和渲染限制都差很多。單一 API 無法把這些都服務好。Netflix 的 BFF 層為每個表面處理翻譯。
Spotify 用類似的模式。網頁播放器、桌面客戶端、行動 App 各有不同的資料需求和效能目標。他們的 BFF 層處理到內部服務的 fan-out,並回傳為每個客戶端量身打造的回應。
這三個案例中,BFF 都是一個結構性的決定,反映了他們的工程團隊怎麼組織,以及他們需要多快出貨。
BFF 模式多了一個網路跳點、一個要營運的服務、一個要管理的部署。這個負擔必須自己證明值得。
如果符合以下情況,就跳過 BFF:
最貴的 BFF,是你在有理由之前就蓋好的那一個。那些脚手架增加了認知負擔,而如果沒有真正的協調問題要吸收,它就變成多餘要維護的程式碼。
BFF 模式是一個 團隊結構工具,只是打扮成架構模式。
它主張:打造 UI 的團隊,應該擁有餵養它的 API 契約。語言選擇(TypeScript)來自於擁有權。安全模型(token 放在伺服器端)來自於 BFF 是一個可信賴的後端層。部署來自於前端和 BFF 一起演進。
當你有協調問題時用它。也就是當前端工程師在等後端團隊重塑回應,或當你在服務多個需求差異很大的客戶端表面時。不要用它來解決你還沒有的問題。
當你真的需要它時,沒有什麼比 BFF 更能給前端團隊對自己的程式碼、以及它消費的資料之間那份契約的完整控制。
© Melvin Laplanche - All rights reserved.