APIコンポジションによるフロントエンドデータ集約の統一
Emily Parker
Product Engineer · Leapcell

バックエンド開発が絶えず進化する中で、リッチでダイナミック、かつパーソナライズされたユーザーエクスペリエンスを提供するという要求は、ますます高まっています。このようなエクスペリエンスを追求するフロントエンドアプリケーションは、しばしば、さまざまな分散バックエンドサービスからのデータを必要とします。従来、Backend for Frontend(BFF)パターンは、このデータを集約、変換し、特定のクライアントタイプやビューに合わせて調整するための、広く採用されているソリューションとして機能してきました。BFFは、フロントエンドの懸念とバックエンドの複雑さを切り離すという否定できない利点を提供しますが、特に保守性、スケーラビリティ、そしてクライアントタイプや機能セットが増加した場合のロジックの重複に関して、それ自体の課題をもたらすこともあります。本稿では、代替的なパラダイム、すなわちAPIコンポジションパターンを活用して、より柔軟なフロントエンドデータ集約を実現し、従来のBFFアプローチからの説得力のある進化を提供する手法について掘り下げます。この方法論が、開発者がより適応性が高く、堅牢なシステムを構築することをどのように支援できるかを考察します。
主要概念の理解
APIコンポジションの具体例に入る前に、関連する主要概念について共通の理解を確立しましょう。
-
Backend for Frontend (BFF): 特定のフロントエンドアプリケーションまたはクライアントタイプごとに専用のバックエンドサービスが作成される設計パターンです。その主な役割は、複数の下流マイクロサービスからデータを集約し、フロントエンドのニーズに合わせて変換し、認証やデータフォーマットのようなクライアント固有の懸念に対処することです。BFFはバックエンドの複雑さを抽象化し、特定のUIのデータ取得を最適化します。
-
マイクロサービス: アプリケーションを、疎結合で、独立してデプロイ可能で、しばしば独立して保守可能なサービスのコレクションとして構造化するアーキテクチャスタイルです。各サービスは通常、特定のビジネス能力を実行します。
-
API Gateway: すべてのクライアントリクエストの単一のエントリポイントとして機能するサービスです。リクエストルーティング、コンポジション、プロトコル変換、認証、認可、キャッシング、レート制限など、その他の横断的懸念に対処できます。APIゲートウェイは一部のデータ集約を実行できますが、通常はクライアント固有のデータ整形よりも、汎用的なルーティングとポリシー施行のために設計されています。
-
API Composition: サービス(フロントエンド自体、API Gateway、または専用のコンポジションサービス)が、複数の下流API呼び出しの結果を動的に組み合わせて、単一のまとまった応答を形成するパターンです。固定されたBFFとは異なり、APIコンポジションは、リクエストのコンテキストや消費クライアントのニーズに基づいたデータの動的な組み立てを重視し、GraphQL、HATEOAS、またはサーバーサイドコンポジションライブラリをしばしば活用します。
APIコンポジションへの移行
従来のBFFに代わるAPIコンポジションの核心原則は、厳格でクライアント固有のバックエンドサービスから、より動的で、宣言的、または設定可能な集約メカニズムへと移行することです。新しいクライアントや機能ごとに新しいマイクロサービス(BFF)を作成するのではなく、オンデマンドで必要なデータを構成できるシステムを目指します。
APIコンポジションの原則
-
宣言的なデータ取得: クライアントは、データを「どのように」取得するかではなく、「何を」必要としているかを指定します。GraphQLのようなツールはここで優れており、フロントエンドが正確なデータ形状を定義できるため、過剰取得や過小取得を回避します。
-
ステートレスな集約ロジック: コンポジションロジックは、理想的にはステートレスで再利用可能であるべきです。これにより、多数の個別のBFFインスタンス内で状態を管理するよりも、複雑さが軽減され、スケーラビリティが向上します。
-
柔軟な変換: 集約はデータをまとめる一方で、変換はそれが適切な形式であることを保証します。APIコンポジションは、柔軟で、しばしばクライアント主導の変換機能を提供するべきです。
-
疎結合: 関心の明確な分離を維持します。バックエンドサービスはそれらのドメインに焦点を当て、コンポジションレイヤーは応答の組み立てに焦点を当てます。
実装戦略とコード例
APIコンポジションを実装するにはいくつかの方法があり、それぞれにトレードオフがあります。
1. GraphQL Gateway
GraphQLはおそらくAPIコンポジションの最も顕著な例です。単一のGraphQLエンドポイントがコンポジションレイヤーとして機能し、クライアントが統一されたスキーマを通じて複数の基盤となるマイクロサービスにクエリを実行できるようにします。
例: Eコマースのフロントエンドが、製品の詳細、そのレビュー、およびその製品に対するユーザーの注文履歴を表示する必要があります。これらはそれぞれProductService
、ReviewService
、OrderService
から取得される可能性があります。
従来のBFFアプローチ:
ProductBFF
サービスは以下を行います。
ProductService
を呼び出して製品詳細を取得します。ReviewService
を呼び出して製品のレビューを取得します。OrderService
(ユーザーコンテキストと共に)を呼び出して製品の注文履歴を取得します。- データを結合して返します。
GraphQLによるAPIコンポジション:
まず、マイクロサービスから型を集約するGraphQLスキーマを定義します。
# Product Service Schema (simplified) type Product { id: ID! name: String! description: String price: Float # ... other product fields } # Review Service Schema (simplified) type Review { id: ID! productId: ID! rating: Int! comment: String # ... other review fields } # Order Service Schema (simplified) type OrderItem { productId: ID! quantity: Int! # ... other order item fields } type Query { product(id: ID!): Product # ... other queries } # Extend Product with reviews and user orders extend type Product { reviews: [Review!] userOrders: [OrderItem!] }
次に、それぞれのマイクロサービスからデータを取得するために、GraphQLゲートウェイ/サーバーにリゾルバーを実装します。
// Example in Node.js with Apollo Server const { ApolloServer, gql } = require('apollo-server'); const axios = require('axios'); // For making HTTP requests to microservices const typeDefs = gql` # ... schema definition from above ... `; const resolvers = { Query: { product: async (_, { id }) => { const response = await axios.get(`http://product-service/products/${id}`); return response.data; }, }, Product: { reviews: async (product) => { const response = await axios.get(`http://review-service/reviews?productId=${product.id}`); return response.data; }, userOrders: async (product, _, { userId }) => { // userId from context/authentication const response = await axios.get(`http://order-service/orders?userId=${userId}&productId=${product.id}`); return response.data; }, }, }; const server = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => { // Extract userId from authentication token, for example const userId = req.headers.authorization || ''; return { userId }; }, }); server.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`); });
フロントエンドクエリ: フロントエンドは単一のGraphQLクエリを発行できます。
query ProductDetails($productId: ID!) { product(id: $productId) { id name description reviews { rating comment } userOrders { quantity } } }
このアプローチは、コンポジションロジックをGraphQLレイヤーに集中させます。フロントエンドは要求したものを正確に取得し、ネットワークペイロードを削減し、クライアントサイドのデータ処理を簡素化します。
2. Server-Side Composition (Proxy/Gateway with Smart Orchestration)
このパターンは、複数のサービスへの呼び出しをオーケストレーションしてそれらの応答を結合できるインテリジェントなプロキシまたはAPIゲートウェイを含みます。単純なプロキシとは異なり、このゲートウェイはデータモデルを理解し、統一された応答を再構築できます。これは、宣言的なルーティングと応答変換をサポートするフレームワークでしばしば活用されます。
例: 上記と同様ですが、GraphQLの代わりに、REST応答をインテリジェントにステッチするゲートウェイを使用します。
実装(例: Apache Camel、Spring Cloud Gateway with custom filters を使用した概念実証):
/api/v1/product-rich-info/{productId}
のようなリクエストを受け取るゲートウェイを検討します。
// Spring Cloud Gateway Predicate/Filter example (conceptual) @Configuration public class GatewayConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("product_rich_info_route", r -> r.path("/api/v1/product-rich-info/{productId}") .filters(f -> f.filter(new ProductRichInfoCompositionFilter())) // Custom filter .uri("lb://product-service")) // Initial call to product service .build(); } // Custom GatewayFilter to compose data public class ProductRichInfoCompositionFilter implements GatewayFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // First, let the request proceed to the product-service return chain.filter(exchange).then(Mono.defer(() -> { ServerHttpResponse response = exchange.getResponse(); // Capture the product data from the initial response // (requires custom BodyCaptureFilter or similar to intercept response body) // For simplicity, let's assume we can get the product ID from the path String productId = exchange.getRequest().getPath().pathWithinApplication().value().split("/")[4]; // Make subsequent calls to review-service and order-service // (using WebClient in a non-blocking fashion) Mono<ProductData> productMono = /* ... extract product data from initial response ... */; Mono<List<ReviewData>> reviewsMono = WebClient.builder().build() .get().uri("http://review-service/reviews?productId=" + productId) .retrieve().bodyToFlux(ReviewData.class).collectList(); Mono<List<OrderItemData>> ordersMono = WebClient.builder().build() .get().uri("http://order-service/orders?userId=someUserId&productId=" + productId) // UserID from JWT/session .retrieve().bodyToFlux(OrderItemData.class).collectList(); // Combine results return Mono.zip(productMono, reviewsMono, ordersMono) .flatMap(tuple -> { ProductData product = tuple.getT1(); List<ReviewData> reviews = tuple.getT2(); List<OrderItemData> orders = tuple.getT3(); // Create a combined JSON response Map<String, Object> combinedResponse = new HashMap<>(); combinedResponse.put("product", product); combinedResponse.put("reviews", reviews); combinedResponse.put("userOrders", orders); // Write combined response back to the client byte[] bytes = new Gson().toJson(combinedResponse).getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(bytes); response.getHeaders().setContentLength(bytes.length); return response.writeWith(Mono.just(buffer)); }); })); } } }
このアプローチは、単純なプロキシよりも設定が複雑ですが、堅牢なゲートウェイ内でコンポジションロジックを集中させます。新しいBFFサービス全体を必要とせずに、動的な組み立てを可能にします。
アプリケーションシナリオ
APIコンポジションパターンは、特に以下に適しています。
- マイクロサービスアーキテクチャ: データが本質的に多くの専門サービスに分散されている場合。
- 迅速なUI開発: 多数のBFFでのバックエンド変更を待つことなく、フロントエンドがデータ要件の変更に迅速に適応できるようになります。
- オムニチャネルエクスペリエンス: Web、モバイル、その他のクライアントが利用できる単一の統一されたデータアクセスレイヤーを提供し、より一貫したデータモデルにつながります。
- 柔軟なデータニーズを持つ公開API: サードパーティ開発者にAPIを提供し、多様なデータ要件を持っている場合。
- バックエンドの重複削減: 複数のBFFサービスにわたる同様の集約ロジックの記述を回避します。
結論
従来のBackend for Frontend(BFF)パターンは、フロントエンドデータ集約のための貴重なメカニズムとして機能してきましたが、複雑なシステムにおける柔軟性、スケーラビリティ、保守性の限界は明らかになることがあります。GraphQLのようなテクノロジーや洗練されたAPIゲートウェイを通じてAPIコンポジションパターンを採用することで、開発者はデータ集約に対して、より動的で、規定の少ないアプローチを実現できます。この移行により、フロントエンドチームはデータ取得においてより大きな制御を得られ、バックエンド開発のオーバーヘッドが削減され、最終的にはより適応性が高く、堅牢で、パフォーマンスの高いアプリケーションアーキテクチャにつながります。APIコンポジションはデータ配信を合理化し、フロントエンド集約を真に柔軟で効率的なものにします。