バックエンド・フォー・フロントエンドパターン:UIが本当に必要とするAPIを作る

バックエンド・フォー・フロントエンドパターン:UIが本当に必要とするAPIを作る

フロントエンド開発者なら誰もが経験したことがあるはずです。APIは技術的に正しいペイロードを返しているのに、実際に作っているものには全く合わない。ホームページでユーザーの名前、アバター、最近の注文3件が必要なのに、代わりに4つのAPIを個別に呼び出し、クライアント側でデータを手動で組み合わせながら、ネットワークが十分速いことを祈っている。

さらにひどい場合もあります。6つのフィールドが必要なのに47個返ってきて、しかも必要なフィールドの名前が usr_acct_disp_nm だったりする。

これはバックエンドの品質問題ではありません。アーキテクチャの問題です。バックエンドは汎用目的で、あるいは全く別のクライアントのために設計されていた。**Backend for Frontend(BFF)**パターンはまさにこの問題を解決するために存在します。

BFFとは何か?

この用語はSoundCloudで最初に作られ、Sam Newmanがマイクロサービス研究の中で公式化しました。核心的なアイデアはシンプルです。すべてのクライアント環境(Web、モバイル、TV、サードパーティ統合)を1つの汎用APIで提供する代わりに、各クライアントに専用のサーバーレイヤーを作るということです。

BFFは次のことを行うサーバーです。

「クライアント環境ごとにBFF1つ」というルールは交渉の余地がありません。モバイルアプリとWebアプリはデータ要件、パフォーマンス予算、インタラクションパターンが異なります。この2つの間でBFFを共有した瞬間、誰にも最適化されていない汎用APIを再び作ることになります。

BFF vs. 通常のバックエンド

通常のバックエンドはビジネスロジックとデータの整合性を担当します。ドメインルールを施行し、データベースを所有し、ドメインが理解する用語でリソースを公開します。ホームページがプロフィールページと少し異なる形のUserを必要としているかどうかを知らないし、知る必要もない。

BFFはプレゼンテーションロジックを担当します。ダウンストリームバックエンドが提供するものを受け取り、特定のフロントエンドが実際に必要なものに変換します。実際には以下のことを意味します。

この区別は重要です。バックエンドエンジニアはエンティティと不変条件で考えます。フロントエンドエンジニアはコンポーネントと画面が必要とするデータで考えます。BFFはその2番目の言語を流暢に話します。

アーキテクチャ:BFFなし vs. BFFあり

Architecture diagram: without BFF vs. with BFF

実際の違いをコードで見ると、こうなります。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で処理されます。フロントエンドコードのあちこちに分散されたり、複数の画面に重複したりしません。

マイクロサービス vs. モノリス環境でのBFF

マイクロサービス:典型的なユースケース

BFFパターンはマイクロサービスの世界で生まれ、その理由があります。バックエンドがユーザー、注文、在庫、通知、決済などの数十のサービスに分散している場合、フロントエンドはそのすべてを把握しなければなりません。小さなUI変更のために3つのバックエンドチームとレスポンス形状を調整しなければならないこともある。

BFFはフロントエンドの単一の契約として機能することでこの問題を解決します。BFFが必要なダウンストリームサービスと通信します。ユーザーサービスがAPIを変更しても、BFFがその変更を吸収します。フロントエンドは気にする必要がない。

これはフロントエンド/バックエンドの境界に適用された腐敗防止レイヤーパターンです。バックエンドの変化の流れからフロントエンドを隔離します。

モノリス:異なる形の利用

BFFはモノリス環境でも同様に有用です。ただし動機が異なります。複数のサービスのオーケストレーション問題ではなく、UIの特定の要件のために設計されていない汎用APIを扱うことです。

このコンテキストでは、BFFは通常モノリスの前にある薄いアダプターレイヤーです。同じコードベース(Next.jsのRoute Handler、モノレポ内のExpressアプリなど)に存在するか、別のサービスとして分離されることがあります。

モノリス環境でのキーとなるルール:BFFを薄く保ちましょう。モノリスからビジネスロジックを複製し始めるBFFはアンチパターンです。翻訳して集約すべきです。ビジネスルールはバックエンドに留まるべきです。

誰がBFFを書くのか?

これがチームがパターンを最も誤って適用している部分です。

**BFFはフロントエンドチームが所有すべきです。**それがパターンの核心です。

バックエンドエンジニアがBFFを所有すると、時間とともに少しずつ汎用APIに変質していきます。レビューサイクルが遅くなります。フロントエンドチームはすぐにデプロイする代わりに、プルリクエストとJIRAチケットを通じてレスポンス形状を交渉することになります。同じ問題のより遅いバージョンに置き換えた状態です。

フロントエンドエンジニアがBFFを所有すると、彼らが契約を所有します。必要なときにデータを再構成します。要求書なしに新しいエンドポイントを追加します。フロントエンドのスピードでデプロイします。

そのためにフロントエンドエンジニアたちはある程度のサーバー側コードを書く必要があります。多くはありません。BFFコードはほとんどがグルーワークです。このサービスを呼び出し、そのレスポンスを変換し、きれいな形で返す。しかしHTTP、認証フロー、非同期I/Oについてある程度慣れている必要があります。複雑なReactアプリケーションを書ける大半のエンジニアなら無理なく対応できます。

所有権の境界はBFF自体です。BFFはダウンストリームサービスを呼び出しますが、ビジネスロジックを所有しません。データベース書き込み、ドメインルール、不変条件はバックエンドに留まります。BFFはプレゼンテーションレイヤーに確実に位置し、この区別は積極的に維持されるべきです。

言語の選択

特にフロントエンドエンジニアたちが主たる責任者である場合、言語の選択は重要です。

**Node.jsランタイム上のTypeScriptがデフォルトの推奨事項です。**理由は実際的です。

Goは、組織にすでにGo専門知識があり、BFFをフルスタックまたはバックエンドエンジニアたちがメンテナンスすると予想される場合の合理的な代替案です。Goは並行処理に優れており、BFFが1つのレスポンスを構成するために複数のダウンストリームリクエストを同時に送信する際に重要です。

しかし言語の選択は所有権モデルに従うべきです。フロントエンドエンジニアたちがBFFを所有するなら、すでに知っている言語を使うべきです。そうでなければ摩擦が生まれ、摩擦は最終的に所有権を再びバックエンドチームに押し戻します。

セキュリティ:BFFが無料で提供するもの

セキュリティはBFFを支持する最も強力な議論の1つですが、一貫して過小評価されています。

トークン保存の問題

シングルページアプリケーションには古いジレンマがあります。アクセストークンをどこに保存するか?localStorageは便利ですが、ページ上のすべてのJavaScriptに公開されます。npmパッケージのサプライチェーン攻撃の時代に実際のリスクです。メモリ内保存はより安全ですが、リフレッシュ時に消えます。HTTP-onlyクッキーは機能しますが、サーバー側の処理が必要です。

BFFはこの問題を完全に解決します。ブラウザは生のアクセストークンを保持しません。

代わりにBFFがトークン交換を行います。アイデンティティプロバイダーから認可コードを受け取り、アクセストークンとリフレッシュトークンに交換し、トークンをサーバー側に保存し、ブラウザに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が独自のデータベースを持ち、ビジネスルールを所有し始めたなら、BFF構築を止めて2番目のバックエンドを作り始めたということです。それは全く別の問題です。

デプロイ

フロントエンドと同じ場所に配置

Next.jsを使っているなら、軽量BFFのためのインフラが既にあります。Route Handlers(App Router)またはAPI Routes(Pages Router)です。フロントエンドとBFFが同じプロジェクトに存在し、1つの単位として一緒にデプロイされます。

小規模から中規模のアプリケーションに適したデフォルトの出発点です。デメリットはフロントエンドと独立してBFFをスケールできないことですが、ほとんどのチームにとってこの制約は無関係です。ここから始めましょう。

モノレポ内の独立したサービス

より大きなアプリケーションでは、モノレポ内でフロントエンドの隣に専用BFFサービスが位置するのが一般的なパターンです。両方ともTypeScript型を共有しますが独立してデプロイされます。フロントエンドはHTTP経由でBFFを呼び出し、BFFはバックエンドサービスに分散します。

この設定は運用上の複雑さが増しますが、独立したスケーリングと明確な分離を提供します。BFFが十分な複雑さを持つときに意味があります。複数の集約パターン、キャッシングレイヤー、接続プーリングなど、運用オーバーヘッドを正当化できるときです。

Kubernetesサイドカー

Kubernetes環境では、BFFはフロントエンドサーバーと同じポッドにサイドカーコンテナとしてデプロイできます。フロントエンドはlocalhostでBFFと通信し、外部ネットワークホップなしでほぼゼロのレイテンシを維持します。常に一緒にデプロイされ、両者間のバージョン不一致を排除します。

避けるべきこと

複数のフロントエンドアプリケーション間でBFFを共有しないでください。そうした瞬間、BFFではなくAPIゲートウェイを作ることになります。解決しようとしていた汎化問題を1レイヤー上で再導入したことになります。

実際の事例

SoundCloudはこのパターンが公式に命名された場所です。彼らのエンジニアリングチームは上で説明したまさに調整問題に直面していました。モバイルとWebクライアントが異なる形のデータを必要とし、共有の汎用APIはフロントエンドとバックエンドチーム間の継続的な交渉を意味していました。クライアント別BFFに分割することでデプロイ速度が回復しました。

Netflixはサポートする各デバイスにBFFレイヤーを運用しています。そしてデバイスの種類は多い。TV、スマートフォン、タブレット、ゲームコンソール、Webブラウザはそれぞれ意味のある異なるパフォーマンス予算とレンダリング制約を持っています。1つのAPIがこれらすべてをうまく提供することはできません。NetflixのBFFレイヤーが各サーフェスに合った変換を処理します。

Spotifyも同様のモデルを使用しています。Webプレーヤー、デスクトップクライアント、モバイルアプリはそれぞれ異なるデータ要件とパフォーマンス目標を持っています。彼らのBFFレイヤーが内部サービスへの分散を処理し、各クライアントに合わせたレスポンスを返します。

3つのケースいずれも、BFFはオプションではありません。エンジニアリングチームがどのように組織されているか、そしてどれだけ速くデプロイする必要があるかを反映する構造的な決定です。

BFFを使うべきでない時

BFFパターンはネットワークホップ、運用するサービス、管理するデプロイメントを追加します。そのオーバーヘッドはその場所を占める価値を証明しなければなりません。

以下の場合はBFFをスキップしましょう。

最も高コストなBFFは必要になる前に作ったものです。足場が認知的オーバーヘッドを追加し、吸収すべき実際の調整問題がなければ、ただメンテナンスしなければならない余分なコードになるだけです。

まとめ

BFFパターンは本質的に、アーキテクチャパターンとして包まれたチーム構造のツールです。

UIを作るチームが、それを供給するAPIの契約を所有すべきだということです。残りはすべてここから導かれます。言語の選択(TypeScript)は所有権から来ます。セキュリティモデル(サーバー側でトークンを保持)はBFFが信頼できるバックエンドレイヤーであることから来ます。デプロイ戦略はフロントエンドとBFFが一緒に進化するという事実から来ます。

調整問題がある時に使いましょう。フロントエンドエンジニアたちがバックエンドチームのレスポンス再構成を待っているか、意味のある異なる要件を持つ複数のクライアント環境を提供している時です。まだない問題を解決するために使わないでください。

必要な時、フロントエンドチームにコードとそれが実行されるデータの間の契約を完全にコントロールできるほど良いことはありません。

© Melvin Laplanche - All rights reserved.