URL-Zustand, Serverzustand und ein BFF: Ein Vite-Stack für interne React-Anwendungen
Warum eine authentifizierte interne React-Plattform von Vite, TanStack Router, TanStack Query sowie einem separaten BFF besser unterstützt werden kann als von Next.js – und wann das nicht mehr zutrifft.
Quellen: Next.js is excellent. That’s exactly why you **shouldn’t** use it everywhere.
Wenn ein neues React-Projekt gestartet wird, lautet die Antwort auf die Frage „Sollten wir Next.js verwenden?“ in der Regel ja, noch bevor jemand die Anwendung beschrieben hat. Für öffentliche, inhaltsreiche Websites ist diese Reaktion oft richtig. Bei einer authentifizierten Operations-Plattform, bei der Benutzer den ganzen Tag damit verbringen, Tabellen zu filtern und Datensätze hinter einem Anmeldeschlüssel zu bearbeiten, lösen viele der stärksten Funktionen des Frameworks Probleme, die die Anwendung gar nicht hat. Dieser Artikel erläutert die Architektur, die eine solche Plattform tatsächlich benötigt, zeigt, wie eine aus React, Vite, TanStack Router, TanStack Query sowie einem Backend-for-Frontend (BFF) bestehende Komponentenstruktur darauf abgebildet werden kann, und endet mit einer Checkliste, um zu entscheiden, wann Next.js letztendlich doch die bessere Wahl ist.
Es gibt nichts hier, was darauf hindeutet, dass Next.js schwach ist. Es gehört zu den besten React-Frameworks, die verfügbar sind. Der Einwand richtet sich dagegen, es standardmäßig zu verwenden, ohne zu prüfen, ob seine Fähigkeiten den Anforderungen entsprechen.
Ein konkretes Szenario
Betrachten wir ein kleines Team von fünf Entwicklern, das eine interne Plattform mit mehr als 100 Routen, mehr als 300 API-Endpunkten sowie etwa einer Million API-Aufrufen pro Monat entwickelt. Sie könnten diese Plattform mit Next.js bauen. Stattdessen wählen sie React mit Vite, TanStack Router für die Routenverwaltung, TanStack Query für die Datenabfrage sowie einen separaten BFF zwischen der Frontend- und Backend-Schnittstelle. Nach sechs Monaten Entwicklung hält diese Entscheidung weiterhin, nicht weil Next.js versagt hätte, sondern weil die Plattform kaum jene Bereiche berührt, in denen Next.js hervorragt.
Für einen umfassenderen Vergleich der beiden Ansätze bei verschiedenen Projekttypen siehe Next.js oder einfaches React: Das Framework an Ihr Projekt anpassen. Der Fokus hier ist enger gefasst: Was eine authentifizierte, datenintensive Anwendung von jeder Schicht benötigt.
Beschreiben Sie die Anwendung, bevor Sie das Framework wählen
Next.js ist zur Standardlösung geworden, weil es so viel in einem Paket bietet: dateibasiertes Routing und Layouts, SSR sowie Server Components, eine Caching-Schicht, Middleware, Backend-Code im selben Projekt, optimierte Bilder sowie ein umfangreiches Ökosystem. Für viele Produkte ist diese Kombination ideal. Die nützlichere erste Frage lautet jedoch, welche Art von Anwendung Sie entwickeln möchten. Zwei React-Anwendungen können architektonisch fast nichts miteinander gemeinsam haben.
Eine öffentliche Verkaufsplattform
Eine öffentliche E-Commerce-Website weist in der Regel Folgendes auf:
- Tausende von Seiten, die jeder öffnen kann
- Verkehr aus Suchmaschinen
- Echte SEO-Anforderungen
- Produktseiten, deren Inhalt von Server-Rendering profitiert
- Teilbare öffentliche URLs und Social-Preview-Funktionen
- Viel statischer oder langsam ändernder Inhalt
Eine interne Betriebsplattform
Ein internes Tool weist in der Regel Folgendes auf:
- Fast alle Bildschirme hinter der Authentifizierung
- Kundensuche, Filter und paginierte Tabellen
- Formulare, Dashboards und Berechtigungsprüfungen
- Häufige Änderungen und nahezu Echtzeit-Daten
- Eine große Menge an Serverzuständen
- Eine hochinteraktive Benutzeroberfläche
- Eine bereits vorhandene API- oder BFF-Schicht
Sowohl Ansätze sind React-Apps, doch sie lösen unterschiedliche Probleme, und die Architektur sollte dies widerspiegeln.
Die Nutzer bedienen die App, anstatt sie nur zu lesen
In einer internen Plattform meldet sich ein Benutzer an und arbeitet den ganzen Tag über innerhalb der Anwendung. Er kommt nicht über ein Suchergebnis, liest keinen Artikel oder durchsucht keine Produktliste. Er sucht, filtert, sortiert, öffnet Dialogfelder, übersendet Formulare, ändert Datensätze, wechselt Tabellen, blättert durch Tabellen, aktualisiert Daten und wechselt zwischen verwandten Entitäten. Er nutzt eine Anwendung, besucht keine Website – und dieser Unterschied bestimmt, welche Framework-Funktionen relevant sind.
SEO ist nicht mehr eine Notwendigkeit
Nehmen Sie einen Pfad wie /customers/12345, der eine authentifizierte Kundenanzeige mit sensiblen Daten zeigt. Sie möchten auf keinen Fall, dass Suchmaschinen diesen Inhalt indizieren. Es ist nicht notwendig, dass Crawler ihn entdecken oder dass die erste HTML-Antwort crawlerfreundlich ist. Die Seite ist wertvoll, weil ein autorisierter Benutzer mit ihr interagiert – nicht weil sie auffindbar ist. Einer der Hauptgründe für die Wahl von Next.js trägt daher nur wenig dazu bei. Das ist ein Missverhältnis der Anforderungen, kein Mangel im Framework.
Wo Server Components passen und wo nicht
Server Components sind ein wirklich nützliches Konzept, weshalb die Frage sorgfältig betrachtet werden sollte. Stellen Sie sich eine Kundenverwaltungsseite mit mehreren Abschnitten vor:
Customer
├── Profile
├── Loans
├── Payments
├── Transactions
├── Documents
└── Activity
Sobald die Seite geladen ist, beginnt der Benutzer, sie zu ändern. Er filtert beispielsweise die Liste der Zahlungen:
Status: Failed
Type: ACH
Date: Last 30 days
Dann werden Seiten umgeblättert, sortiert, gesucht, eine Transaktion eröffnet und etwas geändert. Die Ansicht wird ständig durch Benutzereingaben und aktuelle Serverdaten umgestaltet. Der vorherrschende Ablauf ist nicht dieser dokumentenbasierte Zyklus:
Request → Render document → Read document
Er ähnelt vielmehr einem Interaktionsloop:
User interaction
↓
Application state
↓
Server state
↓
API
↓
Updated UI
Je mehr Ihre Bildschirme dem zweiten Diagramm ähneln, desto stärker bestimmt Ihre Client-Seit-Architektur die Qualität: wie der Zustand gespeichert wird, wie Daten gekacht werden und wie die Benutzeroberfläche auf Veränderungen reagiert. Serverkomponenten können zwar Teile einer solchen Seite darstellen, sie beseitigen jedoch nicht die Notwendigkeit für eine starke Client-Seit-Datenlage.
Die URL als Anwendungsstatus behandeln
Nehmen wir an, ein Bediener befindet sich auf der Kreditkartenseite eines Kunden:
/customer/12345/payments
Er wendet eine Reihe von Filtern an:
status=failed
type=ach
amountMin=500
page=2
Falls diese Filter in der Adresse kodiert sind, sieht die URL so aus:
/customer/12345/payments
?status=failed
&type=ach
&amountMin=500
&page=2
Zu diesem Zeitpunkt ist die URL nicht nur ein Mittel zur Navigation; sie ist Teil des Anwendungszustands. Ein Entwickler kann sie an einen Kollegen weiterleiten, der auf denselben Kunden, dieselbe Registerkarte, dieselben Filter und dieselbe Ergebnisseite gelangt. Sie bleibt auch nach Neu laden bestehen und sorgt dafür, dass die Zurück-Taste so funktioniert, wie die Benutzer es erwarten. Genau hier zeigt sich der Wert eines typsicheren Routers, wenn er auf vielen Bildschirmen eingesetzt wird.
Was TanStack Router beiträgt
TanStack Router betrachtet die Routenverwaltung als zentrales Element des Designs. Routen, Pfadparameter und Suchparameter können im Typensystem definiert werden, was wichtig wird, wenn eine Anwendung mehr als hundert Routen hat. Bei dieser Größe ist die Routenverwaltung nicht mehr nur „welche Komponente wird hier angezeigt“ – sie ist Teil der Architektur. Für eine Route wie folgt:
/customer/$customerId/payments
man möchte:
- klar definierte, typisierte Pfadparameter
Wichtig ist, dass nichts davon davon abhängt, ob eine Seite auf einem Server gerendert wird. Für eine vollständig im Browser laufende interne Anwendung ist das sehr geeignet.
Der Serverzustand ist der eigentliche Schwerpunkt
Ein großer Teil einer großen internen Anwendung dreht sich weniger um Seiten und mehr um entfernte Daten. Die Objekte, die sie ständig liest und schreibt, sehen so aus:
Customers
Loans
Payments
Transactions
Documents
Users
Accounts
Reports
Für jedes davon muss die Frontend-Plattform dieselben Fragen beantworten:
- Sind diese Daten bereits im Speicher?
- Sind sie veraltet?
- Sollten sie neu heruntergeladen werden, und wann?
- Was wird während des Ladevorgangs in der Benutzeroberfläche angezeigt?
- Was passiert, wenn eine Anfrage fehlschlägt?
- Könnte ein anderer Komponente dieses Ergebnis teilen?
- Was ändert sich nach einer Mutation, und welche Abfragen müssen ungültig gemacht werden?
- Was sollte sofort angezeigt werden, wenn der Benutzer zurück navigiert?
- Kann die Benutzeroberfläche optimistisch aktualisiert werden?
Das ist das Management des Server-Zustands, und genau dafür wurde TanStack Query entwickelt: ein Cache, der nach Abfragen sortiert ist, Hintergrund-Neuladungen, Entduplizierung identischer Anfragen, Mutation-Hooks sowie gezielte Ungültigmachung von Daten. Da der Router den URL-Zustand und Query den Server-Zustand verwaltet, hat jede Schicht eine klare Verantwortung, und der lokal im Komponenten befindliche Zustand beschränkt sich auf wirklich lokale Benutzeroberflächenaspekte wie beispielsweise ob ein Dialog geöffnet ist.
Beibehaltung des BFF als API-Grenze
In diesem Szenario ruft die Frontend-Anwendung nicht direkt Dutzende von Microservices auf. Sie kommuniziert mit einem BFF, der wiederum mit den Services spricht. Diese Trennung sorgt dafür, dass das Frontend nichts von den Implementierungsdetails des Backends weiß – einschließlich der Tatsache, ob eine Antwort aus einem oder fünf Services zusammengestellt wird. Der BFF ist für den API-Vertrag verantwortlich.
Zudem ermöglicht sie eine klare Aufteilung bei der Bereitstellung. Die Frontend-Anwendung verwendet statische Ressourcen, die über ein CDN oder einen statischen Host bereitgestellt werden:
Frontend
↓
CDN / static hosting
während der BFF als eigenständiger Service vor den Microservices läuft:
BFF
↓
Microservices
Jede Seite kann unabhängig voneinander erstellt und veröffentlicht werden. Next.js kann durchaus zusammen mit einem BFF bereitgestellt werden. Die eigentliche Frage ist, ob Ihr Frontend-Framework auch die Servergrenze darstellen muss. Wenn die Organisation bereits eine Backend-Schicht hat, die diese Grenze bereitstellt, führt das Hinzufügen einer zweiten Schicht innerhalb des Frontend-Frameworks zu einer Doppelung der Verantwortlichkeiten.
Die Authentifizierung bleibt auf der Serverseite
Auch der Authentifizierungsablauf folgt derselben Trennung. Der Benutzer klickt auf „Anmelden“ und wird zu Okta weitergeleitet. Nach einem erfolgreichen Anmelden erfolgt die Rückruffunktion beim Backend, das den Zugriffstoken auf der Serverseite speichert, wobei Redis für die Session-Speicherung zuständig ist. Der Browser enthält lediglich einen Session-Identifikator in einem sicheren Cookie. Konzeptionell gesehen:
Browser
│
│ Sign in
▼
Okta
│
│ Callback
▼
BFF / Backend
│
▼
Redis
Die React-App ist nicht der Ort, an dem die Authentifizierungsarchitektur liegt; ihre Aufgabe besteht darin, das Produkt zu betreiben, während der BFF die Verantwortung für Sicherheit und den API-Vertrag übernimmt. Ein nützlicher Nebeneffekt ist, dass Zugriffstoken niemals das JavaScript im Browser erreichen, was die Auswirkungen eines XSS-Fehlers verringert. Dies in Next.js umzusetzen ist nicht per se schlechter, aber wenn diese Architektur bereits in der Organisation existiert, ist es einfach und klar, sie außerhalb des Frontends zu belassen.
Ja, Next.js könnte all das bewältigen
Das ist möglich. Man kann die App mit Next.js unter Verwendung von Client-Components bauen, TanStack Query darin einsetzen, die Authentifizierung integrieren, sie mit dem BFF verbinden und sie nach Belieben bereitstellen. Die Funktionalität ist nicht das Problem. Die bessere Frage ist, wie viel vom Framework man tatsächlich nutzen würde. Wenn der größte Teil der Anwendung letztendlich so aussieht:
'use client'
↓
Client-side interactions
↓
API calls
↓
TanStack Query
Dann lohnt es sich, innezuhalten und zu prüfen, ob die serverseitigen Funktionen ausreichen, um Next.js mit seinen Konventionen, Laufzeitumgebung und Upgrade-Möglichkeiten zur Grenze Ihrer Anwendung zu machen.
Jedes Element des komponierbaren Stacks hat eine bestimmte Aufgabe
Für diese Art von Anwendung sind React, Vite, TanStack Router und TanStack Query zusammen mit dem BFF eine sinnvolle Standardauswahl. Es geht nicht darum, Bibliotheken einfach zusammenzukleben, bis man versehentlich sein eigenes Framework erstellt hat. Jedes Element hat eine klare Rolle:
- Vite stellt die Build-Tools und den Entwicklungsserver bereit.
- React rendernt die Benutzeroberfläche.
- TanStack Router ist für das Routing und den URL-Zustand zuständig.
- TanStack Query kümmert sich um den Server-Zustand.
- Der BFF bildet die API- und Sicherheitsgrenze.
Weil die Verantwortlichkeiten klar definiert sind, kann man ein Element ersetzen, ohne die gesamte Anwendung neu überdenken zu müssen.
Die Nachteile des Verzichts auf Next.js
Composable Routes haben ihre Kosten. Man gibt das „alles inklusive“-Erlebnis auf und muss Entscheidungen treffen, die Next.js sonst automatisch für einen trifft: Projektstruktur, Konventionen zum Code-Splitting, wie der statische Host Client-Seiten handhabt sowie das Zusammenhalten mehrerer aktualisierter Bibliotheken. Die Anforderungen können sich außerdem ändern. Wenn später die Hälfte der Anwendung öffentlich zugänglich gemacht werden muss und SEO plötzlich wichtig wird, erscheint Next.js deutlich attraktiver. Dasselbe gilt, wenn man umfangreichen Server-Rendering oder statische Generierung benötigt, wenn Server Components einen echten Mehrwert für das Design bieten oder wenn man tatsächlich Frontend- und Servercode absichtlich in einer einzigen deploybaren Einheit verbinden möchte. Die Entscheidung ist also nicht „Next.js oder React“, sondern welche Architektur am besten zur Anwendung passt.
Eine Checkliste zur Auswahl
Vor dem Commit sollten Sie sich diese Fragen durchdenken:
- Bietet die App eine große öffentliche Sichtbarkeit? Das ist ein großer Vorteil für Next.js.
- Ist SEO wichtig? Dann wird Next.js attraktiver.
- Braucht man Server-Rendering oder statische Generierung? Auch hier gewinnt Next.js.
- Könnten Server Components oder Server Actions das Design erheblich verbessern? Dann sollte man Next.js ernst nehmen.
- Ist die App hauptsächlich authentifiziert und sehr interaktiv? Dann ist React mit Vite die bessere Wahl.
- Führt die Organisation bereits ein BFF oder eine API-Schicht? Eine eigenständige Client-App, die separat bereitgestellt wird, ist eine Überlegung wert.
- Ist der Server-Zustand ein zentrales Problem? Dann wird TanStack Query sehr attraktiv.
- Sind URL-Zustand und komplexe Routing-Strukturen entscheidend? Dann wird TanStack Router sehr attraktiv.
Aus diesen Antworten ergibt sich eine einfache Faustregel: Wenn eine Anwendung eine große Öffentlichkeitsfläche hat, sollte man Next.js ernsthaft in Betracht ziehen. Wenn sie hauptsächlich authentifiziert wird – insbesondere eine interne oder Admin-Plattform – sollte man mit React und Vite beginnen und nur die benötigten Komponenten hinzufügen. Kein dieser Stacke ist im Allgemeinen besser; einer eignet sich besser für ein bestimmtes Problem, wobei dieser Unterschied leicht übersehen wird, sobald ein Framework zur Standardlösung wird.
Zusammenfassung
Die beste Architektur ist nicht die mit der längsten Funktionsliste, sondern die, deren Funktionen die tatsächlichen Probleme lösen. Für ein über eine Anmeldemaske zugängliches Tools mit eigenem Backend, viel Serverzustand, intensiven Interaktionen und sehr wenig öffentlichen Inhalt ist React zusammen mit Vite, TanStack Router und TanStack Query keine Kompromisslösung – es handelt sich dabei um eine bewusste Entscheidung.
- Klassifizieren Sie zunächst die Anwendung: eine Website mit öffentlichem Inhalt oder ein authentifiziertes Tool.
- Betrachten Sie die URL und die Serverdaten als die beiden Hauptspeicher für den Zustand und weisen Sie jedem einen speziellen Tool zu.
- Bewahren Sie die Sicherheits- und API-Grenzen bei, wo Ihre Organisation bereits solche definiert hat.
- Überprüfen Sie die Entscheidung erneut, falls öffentliche Seiten, SEO oder Serverrendering zu tatsächlichen Anforderungen werden.