API-Komposition vereinheitlicht Frontend-Datenaggregation
Emily Parker
Product Engineer · Leapcell

Einführung
In der sich ständig weiterentwickelnden Landschaft der Backend-Entwicklung steigen die Anforderungen an die Bereitstellung reichhaltiger, dynamischer und personalisierter Benutzererlebnisse stetig. Frontend-Anwendungen benötigen für solche Erfahrungen oft Daten aus verschiedenen, getrennten Backend-Diensten. Traditionell diente das Backend for Frontend (BFF)-Muster als weit verbreitete Lösung, um diese Daten zu aggregieren und zu transformieren und sie speziell für verschiedene Client-Typen oder Ansichten aufzubereiten. Obwohl BFFs unbestreitbare Vorteile bei der Entkopplung von Frontend-Anliegen von Backend-Komplexitäten bieten, können sie manchmal eigene Herausforderungen mit sich bringen, insbesondere in Bezug auf Wartbarkeit, Skalierbarkeit und Duplizierung von Logik, wenn die Anzahl der Client-Typen oder Funktionssätze wächst. Dieser Artikel befasst sich mit einem alternativen Paradigma: der Nutzung von API-Kompositionsmustern, um eine flexiblere Frontend-Datenaggregation zu erreichen, die eine überzeugende Weiterentwicklung des herkömmlichen BFF-Ansatzes darstellt. Wir werden untersuchen, wie diese Methodik Entwickler in die Lage versetzen kann, anpassungsfähigere und robustere Systeme aufzubauen.
Verständnis der Landschaft
Bevor wir uns mit den Details der API-Komposition befassen, wollen wir ein gemeinsames Verständnis der wichtigsten Konzepte festlegen:
- Backend for Frontend (BFF): Ein Entwurfsmuster, bei dem für jede spezifische Frontend-Anwendung oder jeden Client-Typ ein dedizierter Backend-Dienst erstellt wird. Seine Hauptaufgabe besteht darin, Daten von mehreren nachgelagerten Microservices zu aggregieren, sie für die Bedürfnisse des Frontends zu transformieren und client-spezifische Aspekte wie Authentifizierung oder Datenformatierung zu handhaben. BFFs abstrahieren die Backend-Komplexität und optimieren die Datenabfrage für eine bestimmte Benutzeroberfläche.
- Microservices: Ein Architekturstil, der eine Anwendung als Sammlung lose gekoppelter, unabhängig bereitstellbarer und oft unabhängig wartbarer Dienste strukturiert. Jeder Dienst führt typischerweise eine spezifische Geschäftsfähigkeit aus.
- API-Gateway: Ein Dienst, der als einzelner Einstiegspunkt für alle Client-Anfragen fungiert. Er kann Anforderungsrouting, Komposition, Protokollübersetzung, Authentifizierung, Autorisierung, Caching und Ratenbegrenzung sowie andere übergreifende Anliegen abwickeln. Obwohl API-Gateways einige Datenaggregationen durchführen können, sind sie in der Regel für allgemeines Routing und Durchsetzung von Richtlinien konzipiert und nicht für die kunden-spezifische Datenformung.
- API-Komposition: Ein Muster, bei dem ein Dienst (der das Frontend selbst, ein API-Gateway oder ein dedizierter Kompositionsdienst sein kann) die Ergebnisse mehrerer nachgelagerter API-Aufrufe dynamisch kombiniert, um eine einzige, kohärente Antwort zu bilden. Im Gegensatz zu einem festen BFF betont die API-Komposition die dynamische Zusammenstellung von Daten basierend auf dem Kontext der Anfrage oder den Bedürfnissen des konsumierenden Clients, wobei oft GraphQL, HATEOAS oder serverseitige Kompositionsbibliotheken verwendet werden.
Der Wechsel zur API-Komposition
Das Kernprinzip hinter der API-Komposition als Alternative zu traditionellen BFFs besteht darin, sich von starren, kundenspezifischen Backend-Diensten hin zu dynamischeren, deklarativeren oder konfigurierbareren Aggregationsmechanismen zu bewegen. Anstatt für jeden neuen Client oder jede neue Funktion einen neuen Microservice (den BFF) zu schreiben, streben wir Systeme an, die die benötigten Daten bei Bedarf zusammenstellen können.
Prinzipien der API-Komposition
- Deklarative Datenabfrage: Clients geben an, welche Daten sie benötigen, und nicht wie sie diese erhalten sollen. Tools wie GraphQL sind hier ausgezeichnet, da sie es dem Frontend ermöglichen, die genaue Datenform zu definieren, wodurch Über- oder Unterabfrage vermieden wird.
- Zustandslose Aggregationslogik: Die Kompositionslogik sollte idealerweise zustandslos und wiederverwendbar sein. Dies reduziert die Komplexität und verbessert die Skalierbarkeit im Vergleich zur Verwaltung des Zustands innerhalb zahlreicher einzelner BFF-Instanzen.
- Flexible Transformation: Während die Aggregation Daten zusammenbringt, sorgt die Transformation dafür, dass sie im richtigen Format vorliegen. Die API-Komposition sollte flexible, oft vom Client gesteuerte Transformationsmöglichkeiten ermöglichen.
- Entkopplung: Eine klare Trennung der Zuständigkeiten aufrechterhalten. Backend-Dienste konzentrieren sich auf ihre Domäne, und die Kompositionsschicht konzentriert sich auf die Zusammenstellung von Antworten.
Implementierungsstrategien und Codebeispiele
Es gibt verschiedene Möglichkeiten, API-Komposition zu implementieren, jede mit ihren Vor- und Nachteilen.
1. GraphQL-Gateway
GraphQL ist vielleicht das bekannteste Beispiel für API-Komposition. Ein einzelner GraphQL-Endpunkt kann als Kompositionsschicht dienen und es Clients ermöglichen, mehrere zugrunde liegende Microservices über ein einheitliches Schema abzufragen.
Beispiel-Szenario: Ein E-Commerce-Frontend muss die Details eines Produkts, seine Bewertungen und die Bestellhistorie des Benutzers für dieses Produkt anzeigen. Diese können jeweils vom ProductService
, ReviewService
und OrderService
stammen.
Traditioneller BFF-Ansatz:
Ein ProductBFF
-Dienst würde:
ProductService
aufrufen, um Produktdetails zu erhalten.ReviewService
aufrufen, um Bewertungen für das Produkt zu erhalten.OrderService
(mit Benutzerkontext) aufrufen, um die Bestellhistorie für das Produkt zu erhalten.- Die Daten kombinieren und zurückgeben.
API-Komposition mit GraphQL:
Definieren Sie zunächst ein GraphQL-Schema, das Typen aus Ihren Microservices aggregiert:
# Product Service Schema (vereinfacht) type Product { id: ID! name: String! description: String price: Float # ... andere Produktfelder } # Review Service Schema (vereinfacht) type Review { id: ID! productId: ID! rating: Int! comment: String # ... andere Bewertungsfelder } # Order Service Schema (vereinfacht) type OrderItem { productId: ID! quantity: Int! # ... andere Bestellpostenfelder } type Query { product(id: ID!): Product # ... andere Abfragen gerichtete } # Erweitern Sie Product mit Bewertungen und Benutzerbestellungen extend type Product { reviews: [Review!] userOrders: [OrderItem!] }
Implementieren Sie dann Resolver in Ihrem GraphQL-Gateway/Server, um Daten von den entsprechenden Microservices ab zurufen:
// Beispiel in Node.js mit Apollo Server const { ApolloServer, gql } = require('apollo-server'); const axios = require('axios'); // Für HTTP-Anfragen an Microservices const typeDefs = gql` # ... Schema-Definition von oben ... `; 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 aus Kontext/Authentifizierung 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 }) => { // userId aus Authentifizierungs-Token extrahieren, z.B. const userId = req.headers.authorization || ''; return { userId }; }, }); server.listen().then(({ url }) => { console.log(`🚀 Server bereit unter ${url}`); });
Frontend-Abfrage: Das Frontend kann dann eine einzelne GraphQL-Abfrage ausführen:
query ProductDetails($productId: ID!) { product(id: $productId) { id name description reviews { rating comment } userOrders { quantity } } }
Dieser Ansatz zentralisiert die Kompositionslogik in der GraphQL-Schicht. Das Frontend erhält genau das, was es verlangt, reduziert die Netzwerk-Payload und vereinfacht die clientseitige Datenverwaltung.
2. Serverseitige Komposition (Proxy/Gateway mit intelligenter Orchestrierung)
Dieses Muster beinhaltet einen intelligenten Proxy oder ein API-Gateway, das Aufrufe an mehrere Dienste orchestrieren und deren Antworten kombinieren kann. Im Gegensatz zu einem einfachen Proxy versteht dieses Gateway das Datenmodell und kann eine einheitliche Antwort rekonstruieren. Dies wird oft mit Frameworks genutzt, die deklaratives Routing und Response-Transformation unterstützen.
Beispiel-Szenario: Ähnlich wie oben, aber anstelle von GraphQL verwenden wir ein Gateway, das REST-Antworten intelligent zusammenfügt.
Implementierung (konzeptionell unter Verwendung von z.B. Apache Camel, Spring Cloud Gateway mit benutzerdefinierten Filtern):
Betrachten Sie ein Gateway, das eine Anfrage wie /api/v1/product-rich-info/{productId}
empfängt.
// Spring Cloud Gateway Prädikat/Filterbeispiel (konzeptionell) @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())) // Benutzerdefinierter Filter .uri("lb://product-service")) // Erster Aufruf des Product-Service .build(); } // Benutzerdefinierter GatewayFilter zum Kombinieren von Daten public class ProductRichInfoCompositionFilter implements GatewayFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // Zuerst die Anfrage zum product-service zulassen return chain.filter(exchange).then(Mono.defer(() -> { ServerHttpResponse response = exchange.getResponse(); // Die Produktdaten aus der ursprünglichen Antwort erfassen // (erfordert benutzerdefinierten BodyCaptureFilter oder ähnliches zum Abfangen des Antwortkörpers) // Der Einfachheit halber gehen wir davon aus, dass wir die Produkt-ID aus dem Pfad erhalten können String productId = exchange.getRequest().getPath().pathWithinApplication().value().split("/")[4]; // Nachfolgende Aufrufe an review-service und order-service tätigen // (nicht-blockierend mit WebClient) Mono<ProductData> productMono = /* ... Produktdaten aus der ursprünglichen Antwort extrahieren ... */; 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 von JWT/Sitzung .retrieve().bodyToFlux(OrderItemData.class).collectList(); // Ergebnisse kombinieren return Mono.zip(productMono, reviewsMono, ordersMono) .flatMap(tuple -> { ProductData product = tuple.getT1(); List<ReviewData> reviews = tuple.getT2(); List<OrderItemData> orders = tuple.getT3(); // Eine kombinierte JSON-Antwort erstellen Map<String, Object> combinedResponse = new HashMap<>(); combinedResponse.put("product", product); combinedResponse.put("reviews", reviews); combinedResponse.put("userOrders", orders); // Kombinierte Antwort zurück an den Client schreiben 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)); }); })); } } }
Dieser Ansatz ist zwar komplexer einzurichten als ein einfacher Proxy, zentralisiert aber die Kompositionslogik in einem robusten Gateway. Er ermöglicht eine dynamische Zusammenstellung, ohne dass ein völlig neuer BFF-Dienst erforderlich ist.
Anwendungsbereiche
API-Kompositionsmuster eignen sich besonders für:
- Microservice-Architekturen: Wo Daten inhärent über viele spezialisierte Dienste verteilt sind.
- Schnelle UI-Entwicklung: Ermöglicht es Frontends, sich schnell an sich ändernde Datenanforderungen anzupassen, ohne auf Backend-Änderungen in zahlreichen BFFs warten zu müssen.
- Omnichannel-Erlebnisse: Bietet eine einzige, einheitliche Datenzugriffsschicht, die von Web-, Mobil- und anderen Clients genutzt werden kann, was zu konsistenteren Datenmodellen führt.
- Öffentliche APIs mit flexiblen Datenanforderungen: Wenn Sie Entwicklern von Drittanbietern eine API anbieten, die unterschiedliche Datenanforderungen haben könnten.
- Reduzierung von Backend-Duplizierungen: Vermeidet das Schreiben ähnlicher Aggregationslogik über mehrere BFF-Dienste hinweg.
Fazit
Obwohl das traditionelle Backend for Frontend (BFF)-Muster ein wertvoller Mechanismus für die Frontend-Datenaggregation war, können seine Einschränkungen in Bezug auf Flexibilität, Skalierbarkeit und Wartbarkeit für komplexe Systeme deutlich werden. Durch die Übernahme von API-Kompositionsmustern, insbesondere durch Technologien wie GraphQL oder ausgeklügelte API-Gateways, können Entwickler einen dynamischeren und weniger vorschreibenden Ansatz zur Datenaggregation erzielen. Dieser Wandel verleiht Frontend-Teams mehr Kontrolle über die Datenabfrage, reduziert den Aufwand für die Backend-Entwicklung und führt letztendlich zu anpassungsfähigeren, robusteren und performanteren Anwendungsarchitekturen. Die API-Komposition optimiert die Datenbereitstellung und macht die Frontend-Aggregation wirklich flexibel und effizient.