すべてのフロントエンドのニーズに対応する単一のNode.js BFF
Olivia Novak
Dev Intern · Leapcell

今日のペースの速いデジタルランドスケープでは、アプリケーションが単一のプラットフォームに限定されることはめったにありません。Webブラウザ、モバイルアプリ、場合によってはデスクトップクライアントを通じてサービスを利用します。このマルチプラットフォームの現実は、バックエンド開発者にとってしばしば大きな課題となります。それは、各固有のフロントエンドの独自のデータおよびインタラクションのニーズを効率的に満たす、単一のモノリシックAPIをどのように作成するかということです。「すべてに適合する」APIを強制しようとすると、通常、データの過剰取得または不足取得、ネットワークペイロードの増加、さまざまなフロントエンドコードベースに散在する複雑なデータ変換ロジックにつながります。これはフロントエンド開発者に負担をかけるだけでなく、バックエンドの進化も複雑にします。解決策は、フロントエンドのAPIニーズをコアバックエンドサービスから戦略的に切り離すことにあり、Node.jsで構築されたBackend for Frontend(BFF)レイヤーが輝くのはまさにここにあります。インテリジェントな仲介者として機能し、各コンシューミングクライアントの正確な要件に合わせてAPIレスポンスを調整し、それによってパフォーマンスを最適化し、フロントエンド開発を簡素化し、独立した進化を可能にします。
Node.js BFFパラダイムの理解
実装の詳細に入る前に、Node.js BFFパターンを支えるいくつかのコアコンセプトを明確にしましょう。
マイクロサービス
A Microservices architecture decomposes a monolithic application into a suite of small, independently deployable services, each performing a specific business function. While offering benefits like scalability and independent development, microservices often expose granular APIs that might not directly map to a frontend's data needs.
フロントエンドのためのバックエンド(BFF)
A Backend for Frontend (BFF) is an architectural pattern where an API gateway or service is specifically built for a single type of frontend application (e.g., a web BFF, a mobile BFF). Unlike a general-purpose API gateway, a BFF focuses on transforming and aggregating data from multiple backend services into a format perfectly suited for its designated frontend. It acts as an abstraction layer, shielding the frontend from the complexities of the underlying microservices.
Node.js
Node.js is an open-source, cross-platform JavaScript runtime environment that executes JavaScript code outside a web browser. Its non-blocking, event-driven architecture, and rich ecosystem of modules (npm) make it an excellent choice for building highly scalable and performant network applications like API gateways and BFFs.
テーラードAPIの原則
BFFパターンの背後にあるコア原則はテーラードAPIです。汎用APIを公開する代わりに、BFFは特定のクライアントに最適化されたAPIを提供します。たとえば、モバイルアプリは、画面スペース、ネットワーク制約、または異なるユーザーフローのために、Webアプリケーションとは異なるデータサブセットまたは異なるデータ構造を必要とする場合があります。BFFはこれらの違いを抽象化し、各フロントエンドにクリーンで最適化されたAPIを提示します。
Node.jsがBFFを強化する方法
Node.jsは、いくつかの主要な特性により、BFFの構築に特に適しています。
- JavaScript Everywhere: フロントエンドとBFFの両方でJavaScriptを使用することで、コード共有、開発者のコンテキストスイッチの削減、統一された開発エクスペリエンスが可能になります。
- 非同期I/O: Node.jsのノンブロッキングI/Oモデルは、APIゲートウェイで一般的な多数の同時リクエストを処理し、イベントループをブロックせずに複数のバックエンドサービスから効率的にデータを集約するのに理想的です。
- 豊富なエコシステム: 膨大なnpmエコシステムは、HTTP通信(例:
axios
、node-fetch
)、ルーティング(例:Express.js、Fastify)、データ変換(例:Lodash)、認証のためのすぐに利用可能なライブラリを提供し、開発を大幅に加速します。 - パフォーマンス: CPUバウンドの強力なリソースではありませんが、Node.jsはI/Oバウンドタスクに優れており、API呼び出しのオーケストレーションにパフォーマンスの高い選択肢となります。
Node.js BFFの構築例
実用的な例で説明しましょう。Products
、Users
、Orders
のために別々のマイクロサービスを持つeコマースアプリケーションを想像してください。Webアプリケーションとモバイルアプリケーションの両方に、商品リストページに関する異なるニーズを提供することを目指しています。
シナリオ:商品リストページ
- Webアプリケーション:
productId
、productName
、description
、price
、imageUrl
、category
、averageRating
が必要です。 - モバイルアプリケーション:
productId
、productName
、thumbnailUrl
、price
、および簡単な表示のための単純化されたshortDescription
が必要です。
コアバックエンドAPI(仮想)
GET /products/{id} # 詳細な製品情報を返します GET /users/{id} # ユーザーの詳細を返します GET /orders/{id} # 注文の詳細を返します
Express.jsを使用したNode.js BFFの実装
まず、BFFの基本的なExpress.jsアプリケーションをセットアップしましょう。
// app.js const express = require('express'); const axios = require('axios'); // バックエンドサービスへのHTTPリクエストを行うため const cors = require('cors'); // さまざまなフロントエンドのCORSを処理するため const app = express(); const PORT = 3000; // ミドルウェア app.use(express.json()); app.use(cors()); // 必要な場合にCORSを特定のオリジンに設定します // --- バックエンドサービスURL(実際のURLに置き換えてください) --- const PRODUCT_SERVICE_URL = 'http://localhost:3001/products'; const RATING_SERVICE_URL = 'http://localhost:3002/ratings'; // 別個のレーティングサービスを想定 // --- 商品リストページ用のWeb BFFエンドポイント --- app.get('/bff/web/products', async (req, res) => { try { // すべての商品を取得します const { data: products } = await axios.get(PRODUCT_SERVICE_URL); // 各商品について、平均レーティングを取得します(集約の例) const productsWithRatings = await Promise.all(products.map(async (product) => { try { const { data: ratingInfo } = await axios.get(`${RATING_SERVICE_URL}/${product.id}/average`); return { productId: product.id, productName: product.name, description: product.longDescription, price: product.price, imageUrl: product.mainImage, category: product.category, averageRating: ratingInfo.averageRating || 0, // レーティングがない場合は0をデフォルトにします }; } catch (error) { console.error(`商品 ${product.id} のレーティング取得に失敗しました:`, error.message); return { productId: product.id, productName: product.name, description: product.longDescription, price: product.price, imageUrl: product.mainImage, category: product.category, averageRating: 0, // エラーを円滑に処理します }; } })); res.json(productsWithRatings); } catch (error) { console.error('Web商品の取得中にエラーが発生しました:', error.message); res.status(500).json({ message: 'Web商品の取得に失敗しました' }); } }); // --- 商品リストページ用のモバイルBFFエンドポイント --- app.get('/bff/mobile/products', async (req, res) => { try { // すべての商品を取得します const { data: products } = await axios.get(PRODUCT_SERVICE_URL); // モバイル固有のニーズに合わせてデータを変換します const mobileProducts = products.map(product => ({ productId: product.id, productName: product.name, thumbnailUrl: product.thumbnailImage, // モバイルは小さいサムネイルを好む場合があります price: product.price, shortDescription: product.shortDescription || product.longDescription.substring(0, 100) + '...', // 説明を切り詰めます })); res.json(mobileProducts); } catch (error) { console.error('モバイル商品の取得中にエラーが発生しました:', error.message); res.status(500).json({ message: 'モバイル商品の取得に失敗しました' }); } }); // BFFサーバーを起動します app.listen(PORT, () => { console.log(`Node.js BFFがポート ${PORT} でリッスンしています`); });
この例では:
-
/bff/web/products
: このエンドポイントはWebアプリケーション専用です。すべての商品を取得し、次に個々の商品から別のRATING_SERVICE_URL
から平均レーティングを非同期に取得し、最後にWebフロントエンドが期待する正確な形式(例:averageRating
、imageUrl
)にデータを整形します。必要に応じて、追加のサービスからのデータ集約が含まれる場合があります(例:ユーザーレビュー、在庫情報)。 -
/bff/mobile/products
: このエンドポイントはモバイルアプリケーション用に設計されています。同じ商品データを取得しますが、異なる変換を適用します。imageUrl
の代わりにthumbnailUrl
を使用します。shortDescription
を生成するためにlongDescription
を切り詰めるか、バックエンドから専用の短い説明フィールドを使用します。- ペイロードサイズと小さい画面での視覚的な煩雑さを減らすために、
description
、category
、averageRating
を省略します。
このBFF内の個別のルーティングとデータ変換ロジックにより、フロントエンドアプリケーションは、基盤となるマイクロサービスの複雑さや互いのデータ要件から効果的に分離されます。各フロントエンドは、過剰取得なしに、またはクライアントサイドでのデータ操作なしに、完全にテーラードされたAPIを利用できるようになり、パフォーマンスの最適化と開発の簡素化につながります。
アプリケーションシナリオ
Node.js BFFパターンは、さまざまなシナリオで非常に役立ちます。
- 多様なクライアントタイプ: Web、iOS、Android、スマートTVなど、データ構造や詳細レベルが大きく異なるフロントエンドがある場合。
- マイクロサービスアーキテクチャ: バックエンドサービスが粒度細かく、それらをフロントエンドに直接公開すると、クライアントサイドでの複雑なオーケストレーションが発生する場合。
- 迅速なフロントエンドイテレーション: フロントエンドチームが、コアバックエンドの変更に影響を与えたり待ったりすることなく、独自のAPIニーズを独立してイテレーションできるようになります。
- レガシーシステム統合: 古い、柔軟性の低いバックエンドシステムの上にファサードとして機能し、新しいフロントエンドにモダンなAPIを提示できます。
- 認証と認可: BFFは認証と認可ロジックを集中化し、セキュリティの追加レイヤーを提供し、クライアントサイドのセキュリティ懸念を簡素化できます。
- パフォーマンス最適化: サーバーサイドでデータを集約およびフィルタリングすることにより、特定のクライアントのデータペイロードとネットワークリクエストを削減します。
結論
Node.js Backend for Frontendパターンは、複雑なマイクロサービスバックエンドから多様なフロントエンドアプリケーションにサービスを提供するという課題に対処する強力なアーキテクチャアプローチです。テーラードされた仲介者として機能することにより、Node.js BFFはデータフローを最適化し、フロントエンド開発を簡素化し、クライアントアプリケーションとコアバックエンドサービスの独立した進化を促進します。各フロントエンドが期待どおりの形式で、まさに必要なデータを受け取ることを保証し、パフォーマンスの向上とより保守性の高い全体的なシステムアーキテクチャにつながります。アプリケーションが複数の異なるクライアントにサービスを提供している場合は、Node.js BFFを検討してください。これは、粒度の細かいバックエンドサービスと特殊なフロントエンドの要求との間のギャップをインテリジェントに橋渡しします。