Stan URL, stan serwera i BFF: stos Vite dla wewnętrznych aplikacji React
Dlaczego autoryzowana wewnętrzna platforma React może lepiej funkcjonować przy użyciu Vite, TanStack Router, TanStack Query oraz oddzielnego BFF niż Next.js, i kiedy to przestaje być prawdą.
Źródła: Next.js is excellent. That’s exactly why you **shouldn’t** use it everywhere.
Gdy rozpoczyna się nowy projekt React, na pytanie „Czy powinniśmy używać Next.js?” zazwyczaj odpowiada się twierdząco, zanim ktokolwiek opisze aplikację. W przypadku publicznych stron o dużej ilości treści ten instynkt jest często słuszny. Jednak w przypadku platform operacyjnych wymagających uwierzytelnienia, gdzie użytkownicy cały dzień spędzają na filtrowaniu tabel i edycji rekordów po zalogowaniu, wiele najpotężniejszych funkcji tego frameworka rozwiązuje problemy, których aplikacja w ogóle nie ma. Ten artykuł omawia architekturę, której takie platformy faktycznie potrzebują, pokazuje, jak zbiór komponentów opartych na React, Vite, TanStack Router, TanStack Query oraz backend-for-frontend (BFF) może się do niej odnieść, i kończy się listą kontrolną pomagającą zdecydować, kiedy Next.js jest ostatecznie lepszym wyborem.
Nic tutaj nie wskazuje na to, że Next.js jest słaby. Jest to jeden z najlepszych frameworki React dostępnych na rynku. Argument dotyczy raczej braku przyjęcia go jako standardu, bez sprawdzenia, czy jego możliwości odpowiadają wymaganiom.
Konkretne scenariusze
Rozważmy mały zespół pięciu inżynierów budujący wewnętrzną platformę z ponad 100 trasami, ponad 300 punktami końcowymi API oraz około milionem wywołań API miesięcznie. Mogliby ją stworzyć za pomocą Next.js. Zamiast tego wybierają React z Vite, TanStack Router do zarządzania trasami, TanStack Query do obsługi danych oraz oddzielny BFF pomiędzy interfejsem użytkownika a usługami backendowymi. Po sześciu miesiącach pracy ten wybór się sprawdza – nie dlatego, że Next.js zawodziłby, ale dlatego, że platforma ledwo korzysta z obszarów, w których Next.js wyróżnia się najbardziej.
Aby uzyskać szersze porównanie obu podejść w zależności od typu projektu, zapoznaj się z Next.js lub zwykły React: dopasowanie frameworka do Twojego projektu. Tutaj skupiamy się na czymś bardziej konkretnym: czego aplikacja wymagająca autoryzacji i obsługująca duże ilości danych potrzebuje od każdej warstwy.
Opisz aplikację przed wyborem frameworka
Next.js stał się standardowym rozwiązaniem, ponieważ łączy w sobie tak wiele funkcji: routowanie i layouty oparte na plikach, SSR oraz Server Components, warstwę cache’owania, middleware, kod backendowy w tym samym projekcie, zoptymalizowane obrazy oraz rozbudowany ekosystem. Dla wielu produktów taka integracja jest idealna. Bardziej przydatne jest jednak pytanie o to, jaki rodzaj aplikacji budujesz – dwie aplikacje React mogą architektonicznie prawie nic ze sobą nie mieć wspólnego.
Publiczna witryna sklepowa
Publiczny sklep internetowy zazwyczaj posiada:
- tysiące stron, które każdy może otworzyć
- ruch pochodzący z silników wyszukiwania
- prawdziwe wymagania SEO
- strony produktów, których treść korzysta z renderowania na serwerze
- dostępne do udostępniania publiczne adresy URL oraz wstępne widoki w mediach społecznościowych
- bardzo dużo treści statycznych lub zmieniających się powoli
Płatforma do operacji wewnętrznych
Narzędzie wewnętrzne zazwyczaj zawiera:
- prawie każdy ekran po procesie autoryzacji
- wyszukiwanie klientów, filtry oraz tabele paginowane
- formularze, panele sterowania oraz sprawdzanie uprawnień
- częste modyfikacje oraz dane niemal w czasie rzeczywistym
- wielką ilość stanu serwera
- bardzo interaktywny interfejs
- istniejącą warstwę API lub BFF
Oba są aplikacjami React, ale rozwiązują różne problemy, a architektura powinna to odzwierciedlać.
Użytkownicy obsługują aplikację, a nie tylko ją czytają
Na platformie wewnętrznej użytkownik loguje się i pracuje w aplikacji przez cały dzień. Nie trafia tam z wyniku wyszukiwania, nie czyta artykułu ani nie przegląda listy produktów. Szuka, filtrowa, sortuje, otwiera okna dialogowe, wysyła formularze, edytuje dane, przechodzi między kartami, przewija tabele, odświeża informacje i przechodzi między powiązanymi elementami. Korzysta z aplikacji, a nie odwiedza stronę internetową, i to zmienia to, które funkcje frameworka są istotne.
SEO przestaje być wymogiem
Weź trasę taką jak /customers/12345, która pokazuje zapis autoryzowanego klienta zawierający dane wrażliwe. Aktywnie nie chcesz, aby silnik wyszukiwania ją indeksował. Nie ma potrzeby, by crawlery ją odkrywały ani aby pierwsza odpowiedź HTML była przyjazna dla crawlerów. Strona jest cenna dlatego, że z nią wchodzi w interakcję uprawniony użytkownik, a nie dlatego, że można ją znaleźć. Jeden z głównych powodów wyboru Next.js ma więc niewielki wpływ. To niezgodność wymagań, a nie wada frameworku.
Gdzie pasują komponenty serwerowe, a gdzie nie
Komponenty serwerowe to naprawdę przydatny model, więc ta kwestia wymaga uważnego rozważenia. Wyobraź sobie stronę zarządzania klientami z kilkoma sekcjami:
Customer
├── Profile
├── Loans
├── Payments
├── Transactions
├── Documents
└── Activity
Gdy tylko strona się załaduje, użytkownik zaczyna ją edytować. Na przykład filtrował listę płatności:
Status: Failed
Type: ACH
Date: Last 30 days
Następnie przewijają stronę, sortują dane, wyszukują informacje, otwierają transakcję i edytują coś. Widok jest ciągle modyfikowany na podstawie wprowadzanych przez użytkownika danych oraz aktualnych informacji z serwera. Dominującym schematem nie jest ten cykl w stylu dokumentu:
Request → Render document → Read document
Jest to znacznie bliższe pętli interakcji:
User interaction
↓
Application state
↓
Server state
↓
API
↓
Updated UI
Im bardziej twoje ekrany przypominają drugi diagram, tym bardziej architektura po stronie klienta decyduje o jakości: o sposobie przechowywania stanu, cacheowania danych oraz reakcji interfejsu na zmiany. Komponenty serwerowe mogą nadal renderować części takiej strony, ale nie eliminują potrzeby silnej warstwy danych po stronie klienta.
Traktowanie URL jako stanu aplikacji
Załóżmy, że użytkownik zajmujący się operacjami znajduje się na karcie płatności danego klienta:
/customer/12345/payments
Stosują zestaw filtrów:
status=failed
type=ach
amountMin=500
page=2
Jeśli te filtry są zakodowane w adresie, URL przyjmuje następujący wygląd:
/customer/12345/payments
?status=failed
&type=ach
&amountMin=500
&page=2
W tym momencie URL to nie tylko środek nawigacji; stanowi on część stanu aplikacji. Jeden inżynier może go skopiować koleżeńsko, a ten trafi na tego samego klienta, tę samą kartę, te same filtry oraz tę samą stronę z wynikami. URL przetrwa również ponowne załadowanie strony i sprawi, że przycisk „Wstecz” będzie działał zgodnie z oczekiwaniami użytkowników. Dokładne zarządzanie tym na wielu ekranach to właśnie sytuacja, w której bezpieczny pod względem typów router odgrywa kluczową rolę.
Czym przyczynia się TanStack Router
TanStack Router traktuje routowanie jako element pierwszorzędnego projektu. Trasy, parametry ścieżki oraz parametry wyszukiwania mogą być zdefiniowane w systemie typów, co jest istotne, gdy aplikacja ma ponad sto tras. Przy takiej skali routowanie to już nie tylko „który komponent ma się tu wyświetlić”; to element architektury. Na przykład dla takiej trasy:
/customer/$customerId/payments
chcesz:
- jasno zdefiniowane, typizowane parametry ścieżki
Kluczowe jest to, że nic z tego nie zależy od tego, czy strona jest renderowana na serwerze. Dla aplikacji wewnętrznej działającej wyłącznie w przeglądarce jest to doskonałe rozwiązanie.
Rzeczywistym punktem ciężkości jest stan serwera
W dużej aplikacji wewnętrznej dużo bardziej niż strony liczą się dane zdalne. Entytety, które jest ona stale odczytująca i zapisująca, wyglądają w ten sposób:
Customers
Loans
Payments
Transactions
Documents
Users
Accounts
Reports
Dla każdej z nich frontend musi odpowiedzieć na te same pytania:
- Czy te dane są już w pamięci?
- Czy stały się przestarzałe?
- Czy należy je ponownie pobrać, i kiedy?
- Co pokazuje interfejs użytkownika podczas ładowania?
- Co się dzieje, gdy żądanie zawodzi?
- Czy inny komponent mógłby dzielić się tym wynikiem?
- Co zmienia się po mutacji i które zapytania muszą zostać unieważnione?
- Co powinno pojawić się natychmiast po powrocie użytkownika?
- Czy interfejs użytkownika może być aktualizowany w sposób optymistyczny?
To jest zarządzanie stanem serwera, i to właśnie jest problem, dla którego został stworzony TanStack Query: cache oparty na zapytaniach, tło do ponownego pobierania danych, eliminacja duplikatów identycznych żądań, hooki do mutacji oraz celowe unieważnianie danych. Ponieważ router zarządza stanem URL, a Query – stanem serwera, każda warstwa ma jasno określoną rolę, a stan lokalny komponentów ogranicza się do rzeczywiście lokalnych kwestii interfejsu, takich jak to, czy okno dialogowe jest otwarte.
Zachowanie BFF jako granicy API
W tym scenariuszu frontend nie łączy się bezpośrednio z dziesiątkami mikrosług. Komunikuje się z BFF, który z kolei łączy się ze służbami. Taka separacja sprawia, że frontend nie jest świadomy szczegółów implementacji backendu, w tym tego, czy odpowiedź jest tworzona na podstawie jednej służby, czy pięciu. BFF odpowiada za umowę API.
Poza tym zapewnia klarowne rozdzielenie etapów wdrażania. Frontend to statyczne zasoby serwowane przez CDN lub host statyczny:
Frontend
↓
CDN / static hosting
podczas gdy BFF działa jako odrębna usługa przed mikrosługami:
BFF
↓
Microservices
Każdą z tych części można budować i wdrażać niezależnie. Next.js może być bez problemu używany razem z BFF. Prawdziwe pytanie brzmi, czy twoja ramka frontendu musi jednocześnie pełnić rolę granicy serwera. Gdy organizacja już posiada warstwę backendu, która zapewnia tę granicę, dodanie drugiej w ramach frameworka frontendu powoduje dublowanie obowiązków.
Autoryzacja pozostaje po stronie serwera
Proces autoryzacji również opiera się na tej samej zasadzie separacji. Użytkownik kliknie „Zaloguj się” i zostanie przekierowany do Okta. Po udanym logowaniu funkcja callback trafia do backendu, który przechowuje token dostępu po stronie serwera, a Redis zajmuje się przechowywaniem sesji. Przeglądarka zawiera jedynie identyfikator sesji w bezpiecznym pliku cookie. Koncepcyjnie:
Browser
│
│ Sign in
▼
Okta
│
│ Callback
▼
BFF / Backend
│
▼
Redis
Aplikacja React nie jest miejscem, w którym znajduje się architektura autoryzacji; jej zadaniem jest obsługa produktu, podczas gdy BFF odpowiada za bezpieczeństwo oraz umowę API. Przydatnym efektem ubocznym jest to, że tokeny dostępu nigdy nie trafiają do JavaScriptu w przeglądarce, co zmniejsza wpływ błędów typu XSS. Wykonywanie tego w ramach Next.js nie jest z natury gorsze, ale gdy taka architektura już istnieje w organizacji, jej umieszczenie poza frontendem jest proste i jasne.
Tak, Next.js może to wszystko zrobić
Może. Można stworzyć aplikację za pomocą Next.js z użyciem komponentów klienckich, zaimplementować w niej TanStack Query, skonfigurować autoryzację, połączyć ją z BFF i wdrożyć w dowolny sposób. Umiejętności nie stanowią problemu. Lepszym pytaniem jest to, jak dużą część frameworka faktycznie zostanie wykorzystana. Jeśli większość aplikacji ostatecznie będzie wyglądać w ten sposób:
'use client'
↓
Client-side interactions
↓
API calls
↓
TanStack Query
Należy wtedy zatrzymać się i zastanowić, czy funkcje strony serwera usprawiedliwiają wybór Next.js jako fundamentu aplikacji, wraz z jej konwencjami, środowiskiem działania i możliwościami aktualizacji.
Każdy element składowej architektury ma jedną rolę
Dla tego typu aplikacji rozsądnym standardem są React, Vite, TanStack Router i TanStack Query, plus BFF. Nie chodzi tu o łączenie bibliotek aż do momentu, gdy przypadkowo stworzymy własną ramę roboczą. Każdy element ma określoną funkcję:
- Vite dostarcza narzędzia do budowania i serwer rozwojowy.
- React renderuje interfejs użytkownika.
- TanStack Router zarządza routowaniem i stanem URL.
- TanStack Query zarządza stanem serwera.
- BFF stanowi granicę API i bezpieczeństwa.
Ponieważ obowiązki są jasno określone, można zastąpić jeden element bez konieczności przemyślenia całej aplikacji od nowa.
Koszty pominięcia Next.js
Route komposable wiążą się z pewnymi kosztami. Tracisz możliwość korzystania z gotowych rozwiązań i musisz sam podejmować decyzje, które normalnie podejmowałby za ciebie Next.js: dotyczące struktury projektu, zasad dzielenia kodu na fragmenty, sposobu obsługi tras po stronie klienta przez hostera statycznego oraz synchronizacji aktualizacji kilku bibliotek. Wymagania mogą się również zmieniać – jeśli połowa aplikacji ma później stać się publiczna, a SEO nagle stanie się ważne, Next.js wydaje się znacznie atrakcyjniejszy. To samo dotyczy sytuacji, gdy potrzebujesz intensywnego renderowania na serwerze lub generowania treści statycznych, gdy komponenty serwerowe dodają rzeczywistej wartości do twojego projektu, lub gdy chcesz celowo połączyć kod frontendowy i serwerowy w jedną aplikację. Dlatego decyzja nie brzmi „Next.js albo React” – chodzi o to, która architektura najlepiej pasuje do aplikacji.
Listwa kontrolna przy wyborze
Zanim podejmiesz ostateczną decyzję, przeanalizuj następujące pytania:
- Czy aplikacja ma znaczną widoczność publiczną? To silna strona Next.js.
- Czy SEO ma znaczenie? Next.js staje się wtedy bardziej atrakcyjny.
- Czy potrzebujesz renderowania na serwerze lub generowania statycznego treści? W tym przypadku również wygrywa Next.js.
- Czy Server Components lub Server Actions znacząco ulepszą projekt? W takim razie trzeba potraktować Next.js poważnie.
- Czy aplikacja wymaga głównie autoryzacji i jest bardzo interaktywna? Wtedy lepiej wybrać React z Vite.
- Czy organizacja już korzysta z warstwy BFF lub API? Warto rozważyć samodzielną aplikację kliencką uruchomioną oddzielnie.
- Czy stan serwera jest głównym problemem? TanStack Query staje się wtedy bardzo atrakcyjny.
- Czy stan URL i złożone routowanie są kluczowe? TanStack Router staje się wtedy bardzo atrakcyjny.
Z tych odpowiedzi wynika prosta zasada praktyczna. Gdy aplikacja ma dużą powierzchnię dostępną dla publiczności, warto poważnie rozważyć Next.js. Jeśli jest przede wszystkim zabezpieczona, szczególnie jeśli chodzi o platformę wewnętrzną lub administracyjną, zacznij od React i Vite, dodając tylko te elementy, które są potrzebne. Żaden z tych zestawów nie jest ogólnie lepszy; jeden nadaje się lepiej do konkretnego problemu, a różnica ta łatwo umyka uwadze, gdy framework staje się standardowym rozwiązaniem.
Podsumowanie
Najlepsza architektura to nie ta z najdłuższym wykazem funkcji, lecz ta, której funkcje rozwiązują problemy, z którymi faktycznie się borykasz. W przypadku narzędzia operacyjnego dostępnego po zalogowaniu, opartego na własnej warstwie backendowej, z intensywnym stanem serwera, licznymi interakcjami i bardzo małą ilością treści publicznych, React z Vite, TanStack Router i TanStack Query nie jest kompromisem – to świadomy wybór.
- Najpierw sklasyfikuj aplikację: stronę z publicznym treścią czy narzędzie wymagające autoryzacji.
- Traktuj adres URL oraz dane serwera jako dwa główne źródła stanu i przeznacz dla każdego z nich oddzielne narzędzie.
- Zachowaj granicę bezpieczeństwa i API tam, gdzie twoja organizacja ją już ustaliła.
- Ponownie przemyśl tę decyzję, jeśli strony publiczne, SEO lub renderowanie na serwerze staną się rzeczywistymi wymaganiami.