Startseite / Artikel

Um Next.js-Route-Handler in eine gezielte BFF-Schicht umzuwandeln

Erfahren Sie, was das Backend-for-Frontend-Muster löst, warum es in Next.js-Anwendungen wieder an Bedeutung gewinnt und wie man vermeidet, dass Route-Handler zu überdimensionierten „Gottobjekten“ werden.

6 Min. Lesezeit1435 Wörter

Ein Entwickler in einem mobilen Team fragte einmal, warum der Bezahlschirm sechs separate API-Aufrufe benötigte, um angezeigt zu werden, wobei drei davon überschneidende Felder aus derselben zugrunde liegenden Service zurücklieferten. Die ehrliche Erklärung war, dass niemand die genauen Anforderungen der Frontend-Plattform kannte, weshalb jedes Team seine eigene Version der Lösung bereitstellte und den Client damit beauftragte, alles zusammenzufügen. Das ist eigentlich kein technisches Problem, sondern ein organisatorisches, und genau dieses Problem sollte das Backend-for-Frontend-Muster lösen. Wahrscheinlich ist das der Grund, warum dieses Muster in den letzten Jahren in mehreren Beiträgen wieder auftauchte, nachdem es jahrzehntelang als fast vergessene Idee in den Engineering-Blogs von Netflix und SoundCloud existierte.

BFF ist keineswegs eine neue Idee. Schon lange vordem „BFF“ zum Standardbegriff wurde, entwickelten zahlreiche Ingenieure ähnliche Konzepte unter anderen Bezeichnungen. Neu ist lediglich, dass dieses Konzept nun im spezifischen Kontext von Next.js-Anwendungen wieder eingeführt wird, die allmählich zu API-Aggregations-Schichten geworden sind – oft ohne dass jemand dies absichtlich entschieden hat. Angesichts dieser Entwicklung lohnt es sich, dieses Muster bewusst zu übernehmen, anstatt zufällig darauf zu stoßen.

Die Struktur des Problems

Dieses Szenario wiederholt sich ständig in leicht abgewandelter Form. Es gibt einige Backend-Dienste – einige werden intern entwickelt, andere sind nachträglich hinzugefügte API-Anbindungen von Anbietern. Zudem gibt es mehrere Clients: eine Webanwendung, eine Mobile-App und möglicherweise ein internes Admin-Tool. Jeder dieser Clients hat andere Anforderungen. Der Mobile-Client benötigt einen leichten Datenträger, da er oft über ein unzuverlässiges Netzwerk läuft. Das Web-Dashboard verlangt eine vollständig denormalisierte Antwort, bei der alle Daten bereits vormergeht, um eine Abfolge von sequenziellen Anfragen zu vermeiden. Das Admin-Tool benötigt rohe, unverarbeitete Daten, da die Nutzer es in der Regel zum Beheben von Problemen verwenden.

Wenn man die BFF-Schicht weglässt, sind nur zwei mögliche Ergebnisse übrig – beide schlecht. Entweder werden die Kerndienste zu überdimensionierten, allgemein einsetzbaren Endpunkten, die versuchen, alle möglichen Nutzer gleichzeitig zufriedenzustellen, was im Grunde ein Friedhof von „Nur noch einen optionalen Abfragesparameter hinzufügen“-Lösungen darstellt, oder jeder Client muss seine eigene Aggregationslogik auf denselben Backend-Diensten neu implementieren. So entstehen schließlich drei leicht unterschiedliche und leicht fehlerhafte Definitionen von „aktivem Benutzer“ in drei separaten Codebasen.

Was eine BFF tatsächlich ist

Ein Backend für das Frontend ist eine leichte Serverschicht, in der Regel eine pro Clienttyp oder -familie, die zwischen der Benutzeroberfläche und den eigentlichen Backend-Diensten liegt. Ihre einzige Aufgabe besteht darin, die Daten für den spezifischen Nutzer, dem sie dienen, zu formatieren. Das webbasierte BFF sowie das mobilgeräteorientierte BFF können zwar auf identische zugrunde liegende Dienste zurückgreifen, doch sie liefern völlig unterschiedliche Antwortformate, da jedes ein eigenständiges Darstellungssproblem löst.

Diese Idee taucht immer wieder in Gesprächen über Next.js auf, weil der App Router im Grunde genommen eine BFF-Schicht kostenlos bereitstellt, sofern man ihn entsprechend verwendet. Route-Handler und Server-Komponenten werden bereits auf dem Server ausgeführt, befinden sich somit zwischen Client und Backend und verfügen über die Möglichkeit, mehrere parallele Anfragen zu senden sowie das kombinierte Ergebnis umzustrukturieren, bevor es den Browser erreicht. Viele Teams, die den App Router übernommen haben, erledigen bereits Aufgaben im Stil einer BFF, ohne dies formell zu benennen oder konsequent anzuwenden.

// app/api/checkout-summary/route.ts
// A web-specific BFF endpoint that aggregates three backend calls
// into the one shape the checkout screen actually needs
export async function GET(req: Request) {
  const userId = getUserIdFromSession(req);  const [cart, pricing, shippingOptions] = await Promise.all([
    fetch(`${CART_SERVICE}/users/${userId}/cart`).then(r => r.json()),
    fetch(`${PRICING_SERVICE}/quote?userId=${userId}`).then(r => r.json()),
    fetch(`${SHIPPING_SERVICE}/options?userId=${userId}`).then(r => r.json()),
  ]);  // Reshape into exactly what the checkout component renders,
  // nothing more. This is the whole point.
  return Response.json({
    items: cart.items.map((i: CartItem) => ({ id: i.id, name: i.name, qty: i.qty })),
    total: pricing.total,
    currency: pricing.currency,
    shipping: shippingOptions.filter((o: ShippingOption) => o.available),
  });
}

Stellen Sie sich nun vor, die Mobile-App wendet sich direkt an diese drei Dienste und erledigt ihre eigene Aggregation in Swift oder Kotlin, wobei sie unabhängig die Logik dafür umsetzt, was als verfügbare Versandoption gilt. Genau so schleicht sich Inkonsistenz ein: Zwei Teams, zwei Programmiersprachen, zwei unterschiedliche Interpretationen derselben Geschäftsregel – früher oder später stimmen diese Interpretationen nicht überein, ohne dass jemand es bemerkt, bis ein Supportantrag auf dem Schreibtisch eines Mitarbeiters landet.

Es wird politisch, nicht nur technisch

Der schwierige Teil dabei, BFF auf die richtige Weise einzuführen, hat wenig mit dem Schreiben des Codes zu tun. Es geht darum, wer die Kontrolle darüber hat. Ein BFF, der ausschließlich für das Web-Team entwickelt wird, funktioniert nur, wenn jemand mit Befugnis ist, ihn schnell zu ändern – idealerweise das Web-Team selbst und nicht eine gemeinsame Plattformgruppe, die an einen zweiwöchigen Release-Zeitplan gebunden ist. Wenn Ihre BFF-Schicht letztendlich in den Händen eines separaten Backend-Teams landet, das sie wie jeden anderen Microservice behandelt und demselben Genehmigungsprozess unterwirft, haben Sie das ursprüngliche Engpassproblem nicht gelöst – Sie haben es lediglich einen Schritt näher zum Client verlagert.

Deshalb passt dieses Muster auch so natürlich in eine Next.js-Codebasis: Die Entwickler, die die Route-Handler schreiben, sind in der Regel dieselben Entwickler, die die Komponenten erstellen, die diese Handler aufrufen. Es gibt kein Ticket zu erstellen, keine Übergabe zwischen Teams und keinen separaten Deployment-Pipeline, der koordiniert werden muss. Wenn Sie die Struktur der Antwort des Checkout-Zusammenfassens ändern müssen, führen Sie diese Änderung im selben Pull Request durch wie bei der Komponente, die sie anzeigt.

Die Falle: Jeden Route-Handler zu einem God Object machen

Es gibt einen Fehlermodus am anderen Ende des Spektrums, der selten erwähnt wird. Sobald ein Team feststellt, dass Route-Handler alles zusammenfassen können, was sie wollen, wird der Wunsch, einen einzigen Endpunkt zu erstellen, der den gesamten anfänglichen Zustand der Anwendung in einem einzigen Aufruf zurückgibt, fast unwiderstehlich – schließlich scheinen weniger Round-Trips offensichtlich vorteilhaft zu sein. Manchmal ist das auch der Fall. Doch das bedeutet auch, dass ein einziger Route-Handler plötzlich sechs unzusammenhängende Funktionen gleichzeitig unterstützen muss, und eine Änderung am Versandmodul zwingt einen nun dazu, den Code im selben Dateiabschnitt wie den Widget für Loyalitätspunkte zu platzieren, weil beides in etwas wie getInitialAppState gepackt wird.

Dieses Muster hält dann, wenn ein BFF-Endpunkt einer einzigen Anzeige oder einer klar abgegrenzten Funktion entspricht und nicht „allem, was die App beim Laden möglicherweise benötigen könnte“. Wenn es Ihnen schwerfällt, den Endpunkt nach einem bestimmten UI-Element zu benennen, ist das in der Regel ein Zeichen dafür, dass er zu umfassend geworden ist.

Es gibt auch ein weiteres Warnsignal, auf das man achten sollte: BFF-Endpunkte, die dabei sind, von der Umformung von Daten zur Neuimplementierung von Geschäftslogik abzuschweifen. Ein Routenhandler, der eigenmächtig entscheidet, ob ein Kunde für kostenlosen Versand in Frage kommt, anstatt einfach den Preisdienst zu befragen und die Antwort weiterzuleiten, ist stillschweigend zu einer zweiten Quelle der Wahrheit für eine Regel geworden, die bereits anderswo im System existiert. Die Aufgabe eines BFF besteht darin, Daten zu übersetzen und zu kombinieren – nicht Entscheidungen zu treffen. Sobald es anfängt, Dinge zu entscheiden, kehrt man zum bereits besprochenen Problem des Abweichens zurück, nur nun in einer Schicht näher am Client, wo es während einer Code-Review viel schwieriger ist, dies zu erkennen.

Mit welchem Ergebnis ich dastehe

Ein BFF muss nicht als eine formelle architektonische Entscheidung mit Diagramm und Wiki-Seite behandelt werden, obwohl es nichts Schlechtes daran gibt, dies zu tun, wenn es zur Arbeitsweise der Organisation passt. Für die meisten Teams, die bereits mit Next.js arbeiten, geht es bei der Einführung weniger darum, etwas Neues einzuführen, sondern vielmehr darum, das Bestehende zu benennen: anzuerkennen, dass die Route-Handler als BFF fungieren, die Fassade aufzugeben, sie seien allgemeine API-Routen, und sie nach den tatsächlichen Anforderungen des Clients zu strukturieren statt nach den Grenzen der Backend-Dienste. Das Muster an sich war nie das Schwierige. Was schwierig ist und was die meisten Teams aufschieben, ist der Umgang damit, dass die Aggregationslogik einen echten Platz benötigt – und diesen Platz dann dem Frontend-Team zu übergeben, das tatsächlich dafür verantwortlich ist, anstatt bis zur Ausführung der Bezahloberfläche mit sechs separaten Aufrufen zu warten, bevor jemand fragt, warum das so ist.

Zusätzliche Literatur

  • Erstellung von Multi-Step AI-Agenten-UIs mit Next.js und dem AI SDK — Erfahren Sie, wie man eine für die Produktion geeignete Benutzeroberfläche für einen AI-Agenten mithilfe typisierter Tools, mehrstufiger Schleifen sowie streamender generativer UI-Komponenten in Next.js entwirft.
  • Automatische Erstellung eines typsicheren Next.js API-Clients aus NestJS Swagger — Lernen Sie, wie man durch die Verwendung von NestJS Swagger und Orval doppelte API-Typen beseitigt und typsichere React Query-Hooks für Next.js automatisch generiert.