こんにちは!
ビジネスアクセラレーション事業部の大瀧優杏です🎀   

急ですが、BFFって知ってますか?
パッとこの用語が出てくると、Best Friend Foreverが真っ先に出てくる方も多いと思います。
平成女児はプリに一度はBFFって書きましたよね?🤣
私もその中の一人でした。😆

IT業界において「BFF」とはBackend For Frontendという別の意味がありました。

今回は、2つの異なるAPIをBFF経由で呼び出す簡易的なwebサイトをAWS LambdaとAPI Gatewayを使って構築した内容を記事にしました!

ぜひ最後までご覧ください!

BFFって?

BFFとは、フロントごとに最適化された専用バックエンドを構築するというアーキテクチャパターンです。

インターフェース固有の要件のみを処理する新しいレイヤーを導入します。このレイヤーは、バックエンド・フォー・フロントエンド(BFF)サービスと呼ばれ、フロントエンドクライアントとバックエンドサービスの間に位置します。アプリケーションがWebインターフェースやモバイルアプリなど、複数のインターフェースをサポートしている場合は、各インターフェースごとにBFFサービスを作成します。

引用: Microsoft Learn

私なりに、ひとことで言い換えると👇

BFF = フロントのためにデータ取得・加工・集約をまとめて行うサーバー。

1つの画面に必要なデータが複数のAPIに散らばっていると、ブラウザがそれぞれを直接叩いて、自分で組み立てる必要があります。
BFFは、その「まとめ役」をフロントの代わりにサーバー側でやってくれる存在です。ブラウザは BFF を1回呼ぶだけ。
あとは BFF が裏で複数のAPIを叩いて、必要な形に整えて返してくれる、というイメージです。

今回は、ユーザー情報と投稿一覧が別々のAPIに分かれているwebサイトを作成しました。
BFFが無ければ、ブラウザはそれぞれのAPIに通信する必要があり、往復が2回発生します。
BFFを挟むことで、クライアントから見た通信回数を減らすことができます。

ハンズオンの内容

完成イメージはこんな感じ👇

Frontend Client(ユーザーのブラウザ)
   └─ GET /api/users/1/dashboard      ← ブラウザが外に出るのはこの1回だけ
        └─ Amazon API Gateway
              └─ AWS Lambda(BFF / Node.js + Express)
                    ├─ GET /users/1            ┐ 並列で取得
                    └─ GET /posts?userId=1     ┘ → まとめて整形して返す
                          (モックバックエンド: JSONPlaceholder)
役割 使ったもの
BFF本体のロジック Node.js + Express
Lambda対応 serverless-http
受付(エンドポイント) Amazon API Gateway
実行環境 AWS Lambda
インフラ定義・デプロイ AWS SAM
まとめる対象のAPI JSONPlaceholder(外部のモックAPI)

「ユーザー情報」と「投稿一覧」という別々のデータを、1回のリクエストでまとめて返すBFFを作成しました。具体的には、/api/users/1/dashboard というURLを叩くと、裏で2つのAPIを呼んでまとめた結果が返ってくる、というものです。


今回はAWS Lambdaが BFFとしての振る舞いを担い、「ユーザー情報」と「投稿一覧」のAPIを一度の通信で呼び出しています。

プロジェクト構成

bff-jsonplaceholder/
├── package.json          … 依存パッケージとnpmスクリプト
├── template.yaml         … AWS SAM(API Gateway + Lambda の定義)
├── src/
│   ├── app.js            … BFF本体(①まとめる + ②整形する。ローカル/Lambda共通)
│   ├── local.js          … ローカル起動用の入口(npm start で使う)
│   └── lambda.js         … serverless-http でLambda化する入口(デプロイで使う)
└── public/
    └── index.html        … 動作確認用のフロント(BFFを叩いて表示するだけ)

app.js

// src/app.js
const express = require("express");
const cors = require("cors");

const app = express();

// フロント(別オリジン)から叩けるように CORS を許可
app.use(cors());

// まとめ先のモックバックエンド
const BASE = "https://jsonplaceholder.typicode.com";

// 動作確認用のヘルスチェック
app.get("/health", (req, res) => {
  res.json({ ok: true, service: "bff" });
});

/**
 * BFF のメインエンドポイント
 * 「ユーザー1人のマイページ」を表示するのに必要なデータを、1回の呼び出しでまとめて返す。
 */
app.get("/api/users/:id/dashboard", async (req, res) => {
  const { id } = req.params;

  try {
    // ① まとめる:2つのAPIを裏で並列に叩く
    const [userRes, postsRes] = await Promise.all([
      fetch(`${BASE}/users/${id}`),
      fetch(`${BASE}/posts?userId=${id}`),
    ]);

    if (!userRes.ok) {
      return res.status(404).json({ error: "user not found" });
    }

    const user = await userRes.json();
    const posts = await postsRes.json();

    // ② 整形する:必要な分だけにして返す(over-fetch 対策)
    res.json({
      user: {
        id: user.id,
        name: user.name,
        email: user.email,
        company: user.company?.name ?? null,
      },
      postCount: posts.length,
      recentPosts: posts.slice(0, 5).map((p) => ({ id: p.id, title: p.title })),
    });
  } catch (err) {
    console.error("[BFF] upstream error:", err);
    res.status(502).json({ error: "upstream error" });
  }
});

module.exports = app;

src/lambda.js

ローカルの Express をそのまま Lambda で動かすために serverless-http を使います。

// src/lambda.js
const serverless = require("serverless-http");
const app = require("./app");

// 同じ Express アプリを Lambda のハンドラとしてラップするだけ。
// API Gateway からのイベントを、Express が解釈できる req/res に変換してくれる。
module.exports.handler = serverless(app);

src/local.js

// src/local.js
const app = require("./app");
const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`BFF running on http://localhost:${port}`);
});

template.yaml

# template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: BFF - API Gateway (HTTP API) + Lambda(Express) -> JSONPlaceholder

Globals:
  Function:
    Timeout: 10
    MemorySize: 256
    Runtime: nodejs20.x      # fetch を使うので 18 以上

Resources:
  BffFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/lambda.handler
      # すべてのパス/メソッドを Express アプリに丸ごと流す(proxy 統合)
      Events:
        ApiProxy:
          Type: HttpApi
          Properties:
            Path: /{proxy+}
            Method: ANY
        ApiRoot:
          Type: HttpApi
          Properties:
            Path: /
            Method: ANY

Outputs:
  BffApiUrl:
    Description: "BFF のベースURL"
    Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"

上記をSAMでbuild,deployします!

成果物



データは JSONPlaceholder のダミーなので、内容は英語のままで読みづらいです💦すみません😢
今回は、「BFFが2つのAPIをまとめて返している」ことを確認することが目的なのでお許しください💦

入力欄に数字を入れて「取得する」を押下すると、入力した数字が id として BFF に投げられます。

すると BFF が裏側で、その id を使ってユーザーAPI(/users/:id)と投稿API(/posts?userId=:id)の2つを並列で叩き、必要な分だけに整形して1つのJSONにまとめて返してくれます。

フロントは返ってきたデータを画面に流し込むだけです。
ユーザー名・メール・投稿件数・最近の投稿が、1回のリクエストでそのまま表示されます。

画面だけじゃ本当に2つのAPIがまとめて返ってくるか判定できないので下記のcurlコマンドでも確かめてみます!

curl https://paxr741iqk.execute-api.us-east-1.amazonaws.com/api/users/1/dashboard

返ってきたのは、

{"user":{"id":1,"name":"Leanne Graham","email":"Sincere@april.biz","company":"Romaguera-Crona"},"postCount":10,"recentPosts":[{"id":1,"title":"sunt aut facere repellat provident occaecati excepturi optio reprehenderit"},{"id":2,"title":"qui est esse"},{"id":3,"title":"ea molestias quasi exercitationem repellat qui ipsa sit aut"},{"id":4,"title":"eum et est occaecati"},{"id":5,"title":"nesciunt quas odio"}]}%

別々のAPIから来た2種類のデータが1つにまとまり、しかも画面に必要な分だけに整形されていることが確認できます。

最後に

「BFF?Best Friend Foreverじゃん!」というところから始まりましたが、実際に手を動かしてみることで、その役割や価値を理解することができました。

今回はシンプルな例でしたが、フロントエンドごとに異なる要件に対応するうえで、BFFはデータの集約や整形を担う重要なレイヤーになります。

また、LambdaとAPI Gatewayを使うことで、シンプルかつスケーラブルに構築できる点も魅力だと感じました。

この記事が、BFFを知るきっかけや実際に試す一歩になれば嬉しいです。

最後までご覧いただき、ありがとうございました!🙌