
フロントエンド開発者なら誰でも、技術的には正しいのに自分の作るものにはまったく合わない API のレスポンスを見たことがあるはずです。ホームページにユーザー名、アバター、最新の注文 3 件が必要だとします。API を 4 回別々に呼び出し、クライアント側でデータをつなぎ合わせ、ネットワークが十分に速くて読み込み順に誰も気づかないことを願います。必要なのは 6 つのフィールドなのに、エンドポイントは 47 個のフィールドを返し、必要なフィールドの名前は usr_acct_disp_nm のようなものです。
これはアーキテクチャの問題です。バックエンドは汎用利用か、まったく別のクライアントのために設計されました。Backend for Frontend(BFF) パターンは、この問題を直すために存在します。
この用語は SoundCloud で生まれ、Sam Newman がマイクロサービスに関する研究の中で整理しました。中核のアイデアは単純です。クライアントごとに専用のサーバー側レイヤーを作るのです。あらゆるクライアント面(ウェブ、モバイル、テレビ、サードパーティ連携)に単一の API で対応しようとする汎用 API は、どれひとつうまく対応できません。
BFF とは次のことを行うサーバーです。
クライアント面ごとに BFF を 1 つ、という原則は譲れません。モバイルアプリとウェブアプリでは、データ要件も性能予算も操作パターンも異なります。両者で BFF を共有すると、誰のためにも最適化しない汎用 API を作ることに逆戻りします。
通常のバックエンドは ビジネスロジックとデータ整合性 を担います。ドメインのルールを適用し、データベースを所有し、ドメイン が理解する言葉でリソースを公開します。ホームページがプロフィールページより少し異なる形の User を必要とすることには関心がありません。
BFF は プレゼンテーションロジック を担います。下流のバックエンドが提供するものを取り、特定のフロントエンドが必要とする形に変換します。実際には次のことを意味します。
この区別は重要です。バックエンドエンジニアはエンティティと不変条件で考えます。フロントエンドエンジニアの枠組みはコンポーネントと、画面が必要とするデータです。BFF はその 2 つ目の言葉を流暢に話します。
実際の違いを見てみましょう。BFF がなければ、ダッシュボードのページは次のように動くかもしれません。
// フロントエンドが3つの別々の呼び出しで画面を組み立てる
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 があれば、フロントエンドは 1 回の呼び出しで必要なものを正確に得られます。
// BFFがこの画面向けに設計されたエンドポイントを提供
const { user, recentOrders, unreadCount } = await fetch("/bff/dashboard");集約と変換のロジックは BFF の中にあります。フロントエンドのあちこちに散らばったり、複数の画面に重複したりしません。
BFF パターンはマイクロサービスの世界で生まれました。それには理由があります。バックエンドが十数のサービス(ユーザー、注文、在庫、通知、請求)に分かれていると、フロントエンドはそのすべてを知る必要があります。小さな UI の変更 1 つで、レスポンスの形を少し変えるためにバックエンドチーム 3 つと調整しなければならないかもしれません。
BFF はフロントエンドの単一の契約として動くことでこれを解決します。BFF は必要な下流サービスと通信します。ユーザーサービスが API を変えても、BFF がその変更を吸収します。フロントエンドには関係ありません。
これはフロントエンドとバックエンドの境界に適用した 腐敗防止層 パターンです。フロントエンドをバックエンド進化の絶え間ない変化から遮断します。
BFF はモノリスでも同じように役立ちます。動機が変わるだけです。複数のサービスを調整しているのではありません。直面しているのは、特定の UI 要件のために設計されていない汎用 API です。
この文脈では、BFF は通常モノリスの前に置く薄い アダプタ層 です。同じコードベース(モジュール、Next.js のルートハンドラ群、モノレポ内の専用 Express アプリ)に置くことも、別のサービスとして存在させることもできます。
モノリスで重要なのは BFF を薄く保つことです。モノリスのビジネスロジックを複製し始める BFF はアンチパターンです。翻訳と集約に徹するべきです。ビジネスルールはバックエンドに属します。
ここでチームは最もよく失敗します。
BFF はフロントエンドチームが所有すべきです。 それがこのパターンのすべてです。
バックエンドエンジニアが BFF を所有すると、それは徐々に汎用 API へと流れていきます。レビューの周期は遅くなります。フロントエンドチームは結局、プルリクエストと JIRA チケットを通じてレスポンスの形を交渉することになります。形はバックエンドチームの承認があって初めて出ていきます。問題 1 つを、同じ問題のより遅い版に取り替えただけです。
フロントエンドエンジニアが BFF を所有すると、彼らが契約を所有します。必要ならデータを整形し直します。依頼を出さずに新しいエンドポイントを追加します。フロントエンドの速度で出荷します。
そのためにはフロントエンドエンジニアがサーバー側のコードを少し書く必要があります。多くはありません。BFF のコードはほとんど糊付け作業です。このサービスを呼び、そのレスポンスを変換し、きれいな形を返す。HTTP と認証フローと非同期 I/O に慣れている必要があります。複雑な React アプリケーションを書けるエンジニアなら、ほとんど苦労せずにこなせます。
所有権の境界は BFF そのものです。BFF は下流サービスを呼びますが、ビジネスロジックは決して所有しません。データベースの書き込み、ドメインルール、不変条件はバックエンドに残ります。BFF はしっかりプレゼンテーション層にあります。その区別は積極的に保つべきです。
重要です。特にフロントエンドエンジニアが主たる所有者の場合はそうです。
Node.js ランタイムと組み合わせた TypeScript を標準で推奨します。 理由は実用的です。
Go は、組織にすでに Go の専門性があり、BFF をフルスタックやバックエンドのエンジニアが維持すると見込まれる場合に、合理的な代替案です。Go は同時ファンアウトをうまく扱います。BFF が 1 つのレスポンスを作るために下流のリクエストを複数同時に送る場合、これは重要です。
言語の選択は所有権モデルに従うべきです。フロントエンドエンジニアが BFF を所有するなら、彼らがすでに知っている言語を使うべきです。それ以外は摩擦を生み、摩擦は結局所有権をバックエンドチームに押し戻します。
セキュリティは BFF を支持する最も強い論拠の 1 つで、絶えず過小評価されています。
シングルページアプリケーションには長年のジレンマがあります。アクセストークンをどこに保存するかです。localStorage は便利ですが、ページ上のあらゆる JavaScript にさらされます。npm パッケージへのサプライチェーン攻撃の時代には、これは現実のリスクです。インメモリはより安全ですが、リフレッシュで消えます。HTTP-only クッキーは機能しますが、サーバー側の処理が必要です。
BFF はこの問題を完全に解決します。ブラウザは生のアクセストークンを決して持ちません。
BFF がトークン交換を行います。ID プロバイダから認可コードを受け取り、アクセストークンとリフレッシュトークンに交換し、トークンをサーバー側に保存し、ブラウザに HTTP-only のセッションクッキーを渡します。以降、フロントエンドからのすべてのリクエストはそのクッキーを伴います。BFF はセッションを探し、実際のトークンを下流リクエストに付けて転送します。トークンは 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 を作るのをやめて 2 つ目のバックエンドを作っています。それはまったく別の問題です。
Next.js を使っていれば、軽量な BFF のための基盤はすでにあります。それが Route Handlers(App Router)または API Routes(Pages Router)です。フロントエンドと BFF は同じプロジェクトに存在し、1 つの単位として一緒にデプロイされます。
これは小規模から中規模のアプリケーションで正しいデフォルトです。代償として、BFF をフロントエンドから独立してスケールできません。多くのチームにとって、その制約は無関係です。ここから始めましょう。
より大きなアプリケーションで一般的なパターンは、モノレポ内でフロントエンドの隣に置く専用の BFF サービスです。両者は TypeScript の型を共有しますが、独立してデプロイされます。フロントエンドは HTTP で BFF を呼びます。BFF はバックエンドサービスにファンアウトします。
この構成は運用が複雑ですが、独立したスケーリングとよりきれいな分離が得られます。BFF が運用の負担を正当化するだけの複雑さを持ったときに意味を持ちます。複数の集約パターン、キャッシュ層、コネクションプーリングがあるという意味です。
Kubernetes 環境では、BFF はフロントエンドサーバーと同じ Pod の サイドカーコンテナ としてデプロイできます。フロントエンドは localhost で BFF と通信し、外部ネットワークホップなしでレイテンシをほぼゼロに保てます。常に一緒にデプロイされるため、両者のバージョンのずれも消えます。
複数のフロントエンドアプリケーションで BFF を共有しないでください。共有した瞬間、API ゲートウェイを作っています。1 層上で、解決しようとしていた一般化の問題を再び持ち込んだことになります。
SoundCloud はこのパターンが正式に命名された場所です。彼らのエンジニアリングチームは、まさに上で述べた調整問題に直面しました。モバイルとウェブのクライアントは異なるデータの形を必要とし、共有の汎用 API はフロントエンドとバックエンドのチーム間で絶え間ない交渉を意味しました。クライアント別の BFF に分けて、出荷速度が回復しました。
Netflix はサポートする各デバイスに対して BFF 層を運用しています。その数は多いです。テレビ、スマートフォン、タブレット、ゲーム機、ウェブブラウザは、性能予算と描画の制約が大きく異なります。1 つの API でそれらすべてをうまく扱うことはできません。Netflix の BFF 層が各面の変換を処理します。
Spotify も同様のモデルを使っています。ウェブプレイヤー、デスクトップクライアント、モバイルアプリは、それぞれデータ要件と性能目標が異なります。彼らの BFF 層が内部サービスへのファンアウトを処理し、各クライアントに合わせたレスポンスを返します。
3 つのケースすべてで、BFF はエンジニアリングチームがどう組織され、どれだけ速く出荷する必要があるかを反映した構造上の決定です。
BFF パターンは、ネットワークホップ、運用するサービス、管理するデプロイを追加します。その負担は居場所を勝ち取る必要があります。
次の場合は BFF をやめましょう。
最も高い BFF は、理由が生まれる前に作ったものです。足場が認知の負担を増やし、吸収すべき実際の調整問題がなければ、維持するための追加コードになります。
BFF パターンは、アーキテクチャパターンに扮した チーム構造のツール です。
UI を作るチームが、それを支える API 契約を所有すべきだと述べています。言語の選択(TypeScript)は所有権から導かれます。セキュリティモデル(サーバー側に置くトークン)は、BFF が信頼できるバックエンド層であることから導かれます。デプロイは、フロントエンドと BFF が一緒に進化することから導かれます。
調整問題があるときに使いましょう。フロントエンドエンジニアがバックエンドチームによるレスポンスの作り直しを待っているとき、または要件が大きく異なる複数のクライアント面にサービスするときです。まだない問題を解決するために使ってはいけません。
必要なとき、フロントエンドチームに自分たちのコードと、それが消費するデータの間の契約を完全に制御させるものとして、これ以上に優れたものはありません。
© Melvin Laplanche - All rights reserved.