1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

BFFとは

Last updated at Posted at 2024-09-08

BFFとは

BFF(Backend for Frontend)の略。

フロントエンド(F)のための(For)バックエンド(B)であり(= BFF)、
バックエンド(B)のための(For)バックエンド(B)ではない(≠ BFB)。

バックエンドとフロントエンドの中間に存在するので
フロントエンド(モバイルアプリ、ウェブアプリなど)にはAPIを提供し、
バックエンドのAPIを呼び出してフロントエンドが必要な情報を作成して返す。

BFFのメリット(簡単に)

  • ①APIの集約: フロントエンドが複数のAPIを個別に呼び出す必要がなくなり、ネットワークの遅延を減少させ、パフォーマンスが向上する
  • ②データの整形: フロントエンドが必要とする形式にデータ整形が行えフロントエンドのロジックがシンプルになる
  • ③フロントエンドごとの最適化: 各フロントエンドの特性に応じた最適なデータ提供が可能になる
  • ④キャッシュの活用: キャッシュを活用することで、バックエンドへのリクエスト数を減らし、パフォーマンスを向上させることができる
  • ⑤開発の分離と独立性: フロントエンドとバックエンドの開発を分離し、各々が独立して開発できる(BFF利用者のメリットでBFF提供者にはデメリット)

上記以外にフロントエンドからのアクセスを必ずBFFを経由させることでバックエンドへの直接アクセスを防ぐことをメリットとして語られることがある。

BFFのメリット(例をあげて)

フロントエンドは会員情報を表示する会員画面を初期表示したいとする。
会員情報を表示する会員画面には、
会員情報だけでなくポイント情報やカード利用情報も表示する必要がある。

バックエンドには、会員番号をキーとして以下の3つのAPIが存在:

  • 会員情報を取得するAPI
  • ポイント情報を取得するAPI
  • カード利用情報を取得するAPI

しかし、会員画面を初期表示するために必要な情報を一度に取得するAPIは存在しない。
バックエンドで
会員画面を初期表示するためのカスタマイズAPIを作成することも可能だが、
業務を意識したカスタマイズAPIをバックエンドに持ちたくない。

フロントエンドで
会員情報、ポイント情報、カード利用情報を取得する3つのAPIを個別に呼び出すことで
実現可能だが、
画面表示に必要な情報をもっと簡単に取得したいし、各々で取得した情報を紐づけるのも面倒。

ここでBFFのメリットが発揮される。
BFFには会員番号をキーとした1つのAPI(①APIの集約)を作成する。
APIは会員情報、ポイント情報、カード利用情報を取得する3つのAPIを呼び出し
会員画面の初期表示に必要な形式に整形されたデータ(②データの整形)を提供する。

上記例から派生して、
フロントエンドが複数の形式、
例えばフロントエンドにiOSとAndroidが存在し同じ機能でも必要な情報が少し違う場合、
バックエンドに影響を与えず専用のAPI(③フロントエンドごとの最適化)を提供できる。

整理すると、
BFFを使用することで、フロントエンドが複数のバックエンドAPIを個別に呼び出す必要がなくなる。
必要なフロントエンドが利用しやすい形式に加工された情報を一度に取得できる。
また、フロントエンドに考慮したAPIをバックエンドに影響を与えず作成できる。
これにより、①APIの集約②データの整形③フロントエンドごとの最適化のメリットが得られる。

バックエンドのコード例

import express from 'express';

const app = express();

// 会員情報を提供するエンドポイント
app.get('/api/member', (req, res) => {
    res.json({ id: 1, name: 'John Doe' });
});

// ポイント情報を提供するエンドポイント
app.get('/api/points', (req, res) => {
    res.json({ memberId: 1, points: 100 });
});

// カード利用情報を提供するエンドポイント
app.get('/api/card-usage', (req, res) => {
    res.json({ memberId: 1, usage: 'Last used on 2023-10-01' });
});

const port = 4000;
app.listen(port, () => {
    console.log(`Backend service is running on http://localhost:${port}`);
});

BFFのコード例

import express from 'express';
import axios from 'axios';

const app = express();

// 会員情報を集約して提供するエンドポイント
app.get('/web/member-info', async (req, res) => {
    try {
        // 複数のAPIリクエストを並行して実行
        const [memberResponse, pointsResponse, cardUsageResponse] = await Promise.all([
            // バックエンドの会員情報APIを呼び出し
            axios.get('http://localhost:4000/api/member'),
            // バックエンドのポイント情報APIを呼び出し
            axios.get('http://localhost:4000/api/points'),
            // バックエンドのカード利用情報APIを呼び出し
            axios.get('http://localhost:4000/api/card-usage')
        ]);

        // 各APIから取得したデータを集約
        const memberInfo = {
            ...memberResponse.data,
            points: pointsResponse.data.points,
            cardUsage: cardUsageResponse.data.usage
        };

        // 集約したデータをフロントエンドに返す
        res.json(memberInfo);
    } catch (error) {
        // エラーハンドリング
        res.status(500).send('Error fetching member information');
    }
});

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

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

BFFのメリットは他にもあり、
フロントエンドは会員情報を表示する会員画面を初期表示したあと、
他の画面に遷移し再び会員情報を表示する会員画面を表示したいとする。
ただし、会員画面に表示する情報は変わっておらず会員画面の表示内容は初期表示時と同じ。

上記で会員画面の初期表示に必要な情報を取得する1つのAPIを作成したので
BFFはバックエンドの
会員情報、ポイント情報、カード利用情報を取得する3つのAPIを個別に実行すればよいが、
バックエンドではDBアクセスが行われ同じ情報を取得するのはリソースがもったいない。

そこでBFFがAPIの戻り値をキャッシュしておくことで3つのAPI呼び出しが不要になり
DBアクセスがなくなり、
API呼び出しがなくなることでオーバーヘッドもなくなる(④キャッシュの活用)。

BFFに限った話ではないがキャッシュの活用についてはいくつかの注意が必要。

  • キャッシュ対象: なんでもキャッシュではなく、再利用性の高いデータに限定することが重要
  • 一貫性: キャッシュはデータのスナップショットなので、データ更新によりキャッシュと最新データが不一致への注意が必要
  • メモリ管理: キャッシュ量が多すぎることでメモリを圧迫しないよう注意が必要
import express from 'express';
import axios from 'axios';
import NodeCache from 'node-cache';

const app = express();
// 会員情報のキャッシュ
const memberCache = new NodeCache({ stdTTL: 60, checkperiod: 120, maxKeys: 100 }); 
// ポイント情報のキャッシュ
const pointsCache = new NodeCache({ stdTTL: 60, checkperiod: 120, maxKeys: 100 }); 
// カード利用情報のキャッシュ
const cardUsageCache = new NodeCache({ stdTTL: 60, checkperiod: 120, maxKeys: 100 }); 

// 会員情報を集約して提供するエンドポイント
app.get('/web/member-info', async (req, res) => {
    const memberCacheKey = 'member-info';
    const pointsCacheKey = 'points-info';
    const cardUsageCacheKey = 'card-usage-info';

    const cachedMemberData = memberCache.get(memberCacheKey);
    const cachedPointsData = pointsCache.get(pointsCacheKey);
    const cachedCardUsageData = cardUsageCache.get(cardUsageCacheKey);

    try {
        let memberPromise;
        let pointsPromise;
        let cardUsagePromise;

        if (cachedMemberData) {
            memberPromise = Promise.resolve(cachedMemberData);
        } else {
            memberPromise = axios.get('http://localhost:4000/api/member').then(response => {
                memberCache.set(memberCacheKey, response.data);
                return response.data;
            });
        }

        if (cachedPointsData) {
            pointsPromise = Promise.resolve(cachedPointsData);
        } else {
            pointsPromise = axios.get('http://localhost:4000/api/points').then(response => {
                pointsCache.set(pointsCacheKey, response.data);
                return response.data;
            });
        }

        if (cachedCardUsageData) {
            cardUsagePromise = Promise.resolve(cachedCardUsageData);
        } else {
            cardUsagePromise = axios.get('http://localhost:4000/api/card-usage').then(response => {
                cardUsageCache.set(cardUsageCacheKey, response.data);
                return response.data;
            });
        }

        const [memberData, pointsData, cardUsageData] = await Promise.all([memberPromise, pointsPromise, cardUsagePromise]);

        const memberInfo = {
            ...memberData,
            points: pointsData.points,
            cardUsage: cardUsageData.usage
        };

        res.json(memberInfo);
    } catch (error) {
        res.status(500).send('Error fetching member information');
    }
});

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

開発プロセスにおいては、
フロントエンドとバックエンドにメリットがあり、
BFFが間を取り持ってくれるので、
フロントエンドは画面にある程度最適化されたAPIが提供される前提で開発が行え、
バックエンドは(完全になくなるわけではないが、)マイクロサービスでAPIの開発が行える。(⑤開発の分離と独立性

BFFについて(個人の感想)

メリットはあるがとりあえずBFFを作ろう!と考えて作るとただのボトルネックになる可能性が高い。
特に、⑤開発の分離と独立性のメリットはBFFへの責任委譲でフロントエンドとバックエンドはBFFがなんとかしてくれると板挟みになる可能性が高いように思える。
維持管理を長く経験してきた身としては新規作成する分には良いが、
不要な維持管理のプラットフォームを増やすのはことになり脱BFFにならないかと後ろ向きに考えてしまう。

そうならないように、
プロジェクトにおいてBFFがどのような価値を提供するか定める。
その価値をブレずに実現するための開発を進めることが必要。

BFFについて(実際に開発してみて)

実際に開発してみると、予想通り⑤開発の分離と独立性のBFFへの責任委譲に関する苦労が大半であった。

【困ったこと】

  • スケジュール
    ⇒ フロントエンドとバックエンドをインプットにするため、両方の進捗影響を受ける。

  • 仕様調整
    ⇒ 業務仕様の決定に関係ないがBFFを経由するということで不要に巻き込まれる。

  • 一貫性(トランザクション制御)
    ⇒ BFFはUI/UXの調整やデータ集約が主な役割のため、トランザクション制御には適しておらず耐性が弱い。一貫性を保証するために複数のAPI呼び出し時にトランザクションをカスタム対応することが多くバックエンド内では容易。

  • 業務ロジック
    ⇒UI/UXを調整するチャネル(ブラウザ・アプリなど)専用の作りであり、ロジックの共有には適さため、業務ロジックは持つべきでない。ただし、バックエンドがドメインをまたいでマッシュアップのような考慮がされていない設計の場合、業務ロジックを乗せざるをえないことがある。

【効果があったこと】

  • 完成状態をイメージした資料作成
    ⇒ フロントエンド~BFF~バックエンドのAPI呼出し関係を表にする。
    (BFF以外の領域を完全には知らないので巻き込むか巻き込めない場合は誤りを恐れず完成状態に持っていく)
1
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?