モバイルチームが商品ページを作っています。汎用 API はスマートフォンが一度も表示しない数十のフィールドを返し、レビュー、在庫、ウィッシュリストにはそれぞれ別の呼び出しが必要です。これらのエンドポイントを変更するには、Web サイトと 3 社のパートナーも担当する API チームを待たなければなりません。
Backend for Frontend(BFF)は、クライアント体験ごとに専用の小さなサーバーサイド API を用意し、そのクライアントを作るチームが所有する方式です。この記事では、パターンの由来、BFF が担うべきこと、API ゲートウェイや GraphQL との比較、OAuth トークンをブラウザに置かない方法、落とし穴、図の描き方を扱います。
パターンの由来
このパターンに名前を付けたのは SoundCloud です。Phil Calçado の 2015 年の解説は、公開 API の上に作られたアプリを取り上げています。プロフィールページは /tracks/1234.json、/tracks/1234/related.json、/users/86762.json、/users/me.json を呼び出し、結果を自分で結合していました。クライアントチームが自分のバックエンドを所有するようになると、結合をそちらに移し、アプリは GET /user-profile/123.json を要求するだけになりました。Web のテックリードだった Nick Fisher がこの用語を作りました。Calçado は「BFF はアプリケーションの一部でした」とまとめています。
Sam Newman の 2015 年 11 月の記事 Backends For Frontends がこれを一般化しました。ユーザー体験ごとに 1 つのバックエンドを置き、その UI を作るチームが保守します。
おしゃべりなクライアントと万能 API
- おしゃべりなクライアント。 Newman の例はウィッシュリストです。まずリストを取得し、次に各アイテムの名前、価格、在庫数を取得します。依存する往復が 2 回になり、モバイル回線で各 150 ms とすると、描画が始まるまでに 300 ms かかります。BFF があれば、スマートフォンの往復は 1 回で済み、依存する呼び出しは 1 ホップが数ミリ秒以下で済むデータセンター内で実行されます。
- 最大の画面に合わせたペイロード。 スマートフォンは表示しないフィールドまでダウンロードします。
- 共有のボトルネック。 どのクライアントチームも、多くは別チームが所有する 1 つのデプロイ単位に変更を並べて待ちます。
BFF が担う責務
| 責務 | BFF に置くもの | BFF に置かないもの |
|---|---|---|
| 集約 | 画面ごとに 1 つのエンドポイントで、サービスを並列に呼び出す | すべてのクライアントが必要とする結合。これはサービスに属します |
| レスポンスの整形 | フィールドの選択、名前の変更、書式設定 | 価格計算などのビジネスルール |
| 認証とセッション | セッションクッキー、トークンの保管と更新 | 最終的な認可。判断はサービスが行います |
| キャッシュ | 短命な集約済みレスポンス | URL だけをキーにしたキャッシュ内のパーソナライズデータ |
| フィーチャーフラグ | このクライアントに表示するバリアント | ドメインの挙動を変えるフラグ |
BFF、API ゲートウェイ、GraphQL の比較
| API ゲートウェイ | BFF | GraphQL サーバー | |
|---|---|---|---|
| 対象 | すべてのクライアント | 1 つのクライアント体験 | 1 つのスキーマを通じてあらゆるクライアント |
| 通常の所有者 | プラットフォームチーム | クライアントチーム | グラフチーム、またはフェデレーションを通じたドメインチーム |
| 役割 | TLS、ルーティング、トークン検査、レート制限 | 集約、整形、セッションとトークン | クライアントがフィールドを選択できる。リゾルバーがサービスを呼び出す |
| 画面が変わると変わるか | いいえ | はい。同じリリースで | クエリだけが変わることが多い |
| 典型的な失敗 | ビジネスロジックが入り込む | 重複、遅いファンアウト | 高コストなクエリ、N+1 のリゾルバー呼び出し、HTTP キャッシュの難しさ |
よくある構成では、エッジに 1 つのゲートウェイを置き、その背後に BFF を置きます。API ゲートウェイのアーキテクチャ図がその例です。共有の 1 つのグラフでクライアントごとの BFF を置き換えることもでき、Microsoft のパターンガイダンスは、フロントエンド専用のリゾルバーがあれば別々の BFF は何も加えないかもしれないと指摘しています。代償はチームの境界です。どのクライアントチームも、1 つのスキーマへの変更を交渉することになります。
所有権とチームの境界
BFF はクライアントチームのものです。Web アプリとその BFF は 1 つのプルリクエストで変更し、1 つのリリースで出荷します。Newman は、モバイルチームがロジックを BFF に移すことで、アプリストアの審査を待たずに出荷できる場合もあると述べています。
BFF はいくつ必要でしょうか。Newman は Stewart Gleadow の言葉を引いています。1 つの体験に 1 つの BFF です。REA Group は iOS と Android で別々の BFF を運用していました。SoundCloud は iOS と Android のリスナーアプリで 1 つを共有しており、エンジニアは Newman に、見直すかもしれないと話しました。Pete Hodgson の考え方は、チームの境界に従うことです。エンジニアリング組織図には、BFF ごとに所有チームがちょうど 1 つだけ示されているはずです。
パートナー向け BFF は、安定したバージョン付きの契約を保ち、サービスが変わるたびにそれをサービスへ対応付けます。そのため、内部の作業がパートナーの連携を壊しません。パートナーをバージョン付きのサービス API へ直接ルーティングする方法もあります。
ブラウザアプリのトークンハンドラーとしての BFF
OAuth を自分で実行するシングルページアプリは、トークンをブラウザに保持します。そこでは、注入されたスクリプトに盗まれる恐れがあります。ブラウザアプリがトークンをどこに保管できるかと、それぞれのリスクは JWT のガイドで比較しています。ブラウザベースアプリ向け OAuth 2.0 である RFC 10017 は、2026 年 8 月に Best Current Practice(BCP 212)になりました。3 つのアーキテクチャを、安全性の高い順に並べています。
| パターン | トークンの置き場所 | 注入された JavaScript にできること | RFC 10017 の評価 |
|---|---|---|---|
| BFF | サーバーのみ | ページが開いている間、BFF 経由でリクエストを送る | ビジネス向けや機微なアプリには強く推奨 |
| トークン仲介バックエンド | アクセストークンはブラウザ、リフレッシュトークンはサーバー | アクセストークンを盗み、バックエンドに新しいものを要求する | 完全な BFF が現実的でない場合のみ |
| ブラウザベースの OAuth クライアント | すべてのトークンがブラウザ | すべてのトークンを盗み、新しいフローを実行する | ビジネス向けや機微なアプリには非推奨 |
ここでの BFF は機密(confidential)OAuth クライアントです。PKCE 付きの authorization code フローを実行し、自身の資格情報でコードを交換し、トークンをユーザーのセッションに紐づけて保管し、転送するすべての呼び出しにアクセストークンを付けます。ブラウザが持つのはクッキーだけです。OAuth 2.0 フロー図で code フローを順に確認できます。
RFC の主なルールは次のとおりです。
- クッキー:
SecureとHttpOnlyは必須です。SameSite=Strict、Path=/、Domainなし、__Host-Http-のような名前プレフィックスが推奨されています。Chrome と Firefox はこのプレフィックスを強制します。Safari は、執筆時点(2026 年 11 月)では__Host-の部分だけを適用します。 - CSRF: 対策は必須です。すべての API 呼び出しに固定のカスタムヘッダーを付けると、クロスオリジンのリクエストでは CORS プリフライトが強制されます。
- プロキシ: 許可リストにあるホストにのみ転送します。
- セッション状態: サーバー側のストア(RFC は小規模な場合にのみ推奨)、または RFC 6265 がブラウザに対応を求める 1 クッキーあたり 4,096 バイトの範囲内で、クッキーに暗号化したトークンを入れます。セッションはリフレッシュトークンと同じ期間だけ有効にします。
Set-Cookie: __Host-Http-sid=3f9c...; Secure; HttpOnly; SameSite=Strict; Path=/
// Same-origin fetch sends the cookie by default.
await fetch("/bff/orders", {
method: "POST",
headers: { "Content-Type": "application/json", "X-CSRF": "1" },
body: JSON.stringify(order),
});
BFF はクロスサイトスクリプティングを防ぎません。注入されたコードは、ページが開いている間は BFF を呼び出せるため、Content Security Policy を維持してください。ネイティブアプリは代わりに RFC 8252 に従い、システムブラウザで PKCE 付きの code フローを実行するので、モバイル BFF は通常、ベアラートークンを検証するだけです。
落とし穴
- ロジックの重複。 Newman は、BFF を結合させる共有コードより、多少の重複を好みます。同じロジックが 3 回目に現れたら切り出します。技術的なコードならライブラリに、ビジネスロジックならドメインサービスにします。SoundCloud は、約 5 つの BFF がそれぞれユーザープロフィールを組み立てていたことから、UserProfileService を作りました。
- モノリスになった BFF。 警告サインは、ドメインデータやビジネスルールを持っている、他の BFF から呼ばれている、複数のチームがコミットしている、のいずれかです。Web BFF では計算されるのにモバイル BFF では計算されない割引は、顧客に見つかるバグです。
- ファンアウトのレイテンシ。 BFF が 5 つのサービスを並列に呼ぶと、少なくとも 1 つが自身の p99 より遅く応答する確率は 1 − 0.99^5 ≈ 4.9% になります。つまり BFF の p95 は、おおよそサービスの p99 です。Dean と Barroso の The Tail at Scale が対策を扱っています。
- ホップがもう 1 つ増える。 BFF ごとにレイテンシ、デプロイ、オンコール体制が増え、IP アドレスでレート制限するサービスには BFF のアドレスしか見えなくなります。
タイムアウト、部分レスポンス、サーキットブレーカー
画面ごとにデッドラインを決めて分割します。BFF で 800 ms なら、呼び出しごとに 300 ms のタイムアウトで 1 回のリトライの余地が残ります。独立した呼び出しは並列に実行し、デッドラインを下流に渡します。
次に、どのデータが必須かを決めます。Newman のウィッシュリストにはリストが必要ですが、在庫数がなくても描画できます。手元にあるものを返し、欠けているものを示します。
{
"items": [{ "id": "sku-123", "name": "Trail shoe", "price": "$89.00" }],
"stock": null,
"errors": [{ "section": "stock", "reason": "timeout" }]
}
下流のサービスごとに専用のサーキットブレーカーとコネクションプールを持たせ、遅いサービスがすべてのワーカーを占有せず、すばやく失敗するようにします。しきい値とフォールバックはサーキットブレーカーパターンで扱っています。リトライは冪等な読み取りだけに、1 つの層でのみ行い、集約済みレスポンスのキャッシュは、最も新しい部分が許す期間を超えないようにします。
BFF のアーキテクチャ図
ダイアグラムの内容
- Shared services (domain teams)
- Identity provider
- Session store
- Web app
- Web BFF
- Mobile app
- Mobile BFF
- Partner system
- Partner BFF
- Catalog service
- Orders service
- Profile service
- session cookie only
- bearer access token
- client credentials token
- parallel calls + token
- parallel, fewer fields
- stable v1 mapping
- tokens by session ID
- 1. log in (redirect)
- 2. code + PKCE → tokens
図の読み方は次のとおりです。
- クライアント体験ごとに 1 行, 所有チームで色分けしています。各クライアントは自分の BFF だけを呼び出します。
- 各 BFF は共有サービスへ扇状に広がります。 矢印がフレームで終わっているのは、画面が必要とするサービスを呼び出すためです。
- 破線のエッジはサインイン専用です。 ブラウザが ID プロバイダーへリダイレクトされ、その後 Web BFF がコードをトークンに交換します。これは OpenID Connect によるサインインと同じ手順です。
- トークンはサーバーに残り, セッション ID をキーにセッションストアに保管されます。暗号化したクッキーでも機能し、RFC 10017 は小規模な構成を超える場合にそちらを推奨しています。
- Markdown ブロックには、クッキーの設定と、画面ごとの予算の例を記載しています。
BFF の図をわかりやすく描くには
- クライアントごとに 1 行を割り当て、 BFF とともに、所有チームのラベルを付けます。
- 各エッジを通るものにラベルを付けます。セッションクッキー、ベアラートークン、クライアント資格情報などです。
- トークンの置き場所を示します。ストアか、暗号化したクッキーかを明記します。
- ゲートウェイは別に描きます。 ゲートウェイと BFF を 1 つの箱にすると、所有関係が見えなくなります。
- ハブに注意します。 すべてのクライアントにつながる 1 つの BFF の箱は、汎用バックエンドです。
- エンドポイントの一覧は省きます。 BFF ごとにサービス境界へ向かうラベル付きエッジを 1 本引けば十分です。サービス側は、BFF の背後にあるマイクロサービスの図を別に描いて扱います。
Archboard で作る
- Google または GitHub でサインインし、ダイアグラムを作成します。
- C4 モデル パックから、Web アプリとモバイルアプリに コンテナ を 1 つずつ、パートナーに 外部システム を 1 つ、行ごとに追加します。
- 各クライアントの隣に、その BFF 用の コンテナ を追加し、技術と説明を入力します。
- 基本 パックから、Shared services (domain teams) という名前の フレーム を追加し、3 つの コンテナ ノード(catalog、orders、profile)を入れます。
- Web の行の上に、セキュリティ パックの IDプロバイダー と、セッションストアとして データ パックの Redis ノードを追加します。
- 各クライアントを BFF に、各 BFF をフレームに接続し、エッジにラベルを付け、サインインの 2 本のエッジを破線にして、行ごとにチームの色を付けます。
- 基本パックの Markdown ブロックにクッキーの設定と予算を書き、チーム名として基本パックの テキスト ラベルを追加します。
サービスがすでに Mermaid のフローチャートにあるなら、Mermaid コンバーターで編集可能な最初の下書きが得られます。この例で使うダイアグラムノードは 13 個で、Free プランの範囲に収まります。
よくある質問
Backend for Frontend(BFF)パターンとは何ですか? Web アプリ、モバイルアプリ、パートナー連携など、1 つのクライアント体験のために作られ、そのクライアントのチームが所有するサーバーサイド API です。バックエンドの呼び出しを集約し、レスポンスを整形します。
BFF と API ゲートウェイの違いは何ですか? ゲートウェイはすべてのクライアントを対象に、ルーティング、トークン検査、レート制限を担当します。BFF は 1 つのクライアントを対象にし、その画面とともに変わります。多くのシステムでは、複数の BFF の前にゲートウェイを置きます。
GraphQL は BFF パターンの代わりになりますか? なりえます。クライアントが必要なフィールドを選べるためです。ただし、スキーマとクエリの制限は誰かが所有する必要があり、1 つのクライアント向けに作られた GraphQL サーバーは、それ自体が BFF です。
BFF の認証はどのように機能しますか? BFF は機密 OAuth クライアントです。PKCE 付きの authorization code フローを実行し、トークンをサーバーに保持し、ブラウザには HttpOnly のセッションクッキーだけを渡します。
BFF はいくつ必要ですか? ユーザー体験ごとに 1 つで、チームに合わせます。同じ体験と所有チームを持つ 2 つのアプリは、1 つを共有できます。