Strona główna / Artykuły

Przekształcanie obsługujących trasy w Next.js w celową warstwę BFF

Dowiedz się, co rozwiązuje wzorzec Backend for Frontend, dlaczego ponownie pojawia się w aplikacjach Next.js oraz jak uniknąć przekształcania obsługujących trasy w rozbudowane, nadmiernie złożone obiekty.

6 min czytania1435 słów

Jeden z programistów z zespołu mobilnego zapytał kiedyś, dlaczego ekran płatności wymaga sześciu oddzielnych wywołań API, aby się załadować, przy czym trzy z nich zwracają nakładające się pola pochodzące z tego samego podstawowego serwisu. Szczera odpowiedź brzmiała, że nikt nie określił dokładnie, czego faktycznie potrzebuje interfejs użytkownika, więc każdy zespół dostarczał własną wersję rozwiązania i pozostawiał klientowi zadanie ich połączenia. To nie jest tak naprawdę problem techniczny, lecz organizacyjny, i właśnie to ma rozwiązać wzorzec Backend for Frontend. Prawdopodobnie dlatego wzorzec ten pojawił się ponownie w kilku ostatnich publikacjach, po tym jak przez lata pozostawał pomijaną ideą z blogów inżynieryjnych Netflix i SoundCloud sprzed dekady.

BFF to zdecydowanie nie nowy pomysł. Wiele inżynierów budowało takie rozwiązania pod inną nazwą, zanim „BFF” stało się standardowym terminem. Nowością jest to, że jest ono ponownie wprowadzane w specyficznym kontekście aplikacji Next.js, które stopniowo przekształciły się w warstwy agregacji API, często bez żadnej celowej decyzji. Biorąc pod uwagę tę tendencję, warto celowo przyjąć ten wzorzec, zamiast natknąć się na niego przypadkowo.

Kształt problemu

Sytuacja ta powtarza się w nieco innych formach. Masz kilka usług backendowych – niektóre opracowane wewnętrznie, inne to API dostawców dodane później. Masz również kilku klientów: aplikację internetową, aplikację mobilną oraz być może narzędzie administracyjne wewnętrzne. Każdy z tych klientów ma inne wymagania. Klient mobilny potrzebuje lekkiego obciążenia danych, ponieważ często działa w niesolidnej sieci. Panel internetowy wymaga w pełni zdenormalizowanej odpowiedzi, w której wszystkie dane są już połączone, aby uniknąć serii kolejnych żądań. Narzędzie administracyjne potrzebuje surowych, nieprzetworzonych danych, ponieważ osoby je używające zazwyczaj naprawiają jakieś problemy.

Pomijając warstwę BFF, mamy do wyboru jedno z dwóch niekorzystnych rozwiązań. Albo nasze usługi główne rosną w objętości, stając się przeciążonymi punktami końcowymi o zastosowaniu ogólnym, które próbują zadowolić wszystkich potencjalnych użytkowników jednocześnie – co w praktyce przypomina cmentarzysko pomysłów typu „dodaj jeszcze jeden opcjonalny parametr zapytania” – albo każdy klient musi samodzielnie implementować własną logikę agregacji na bazie tych samych usług backendowych. W ten sposób pojawiają się trzy nieco różne i błędne definicje „aktywnego użytkownika” rozproszone w trzech oddzielnych bazach kodu.

Czym właściwie jest BFF

Tło dla interfejsu użytkownika to lekka warstwa serwerowa, zazwyczaj jedna na typ klienta lub rodzinę klientów, znajdująca się pomiędzy interfejsem graficznym a rzeczywistymi usługami tła. Jej jedynym zadaniem jest kształtowanie danych dostosowanych do konkretnego użytkownika, któremu służy. BFF przeznaczone do pracy w sieci i BFF przeznaczone do pracy w urządzeniach mobilnych mogą korzystać z identycznych usług podstawowych, ale zwracać zupełnie różne formaty odpowiedzi, ponieważ każde z nich rozwiązuje odrębny problem prezentacji danych.

Ta koncepcja często pojawia się w rozmowach o Next.js, ponieważ App Router w zasadzie dostarcza warstwę BFF za darmo, pod warunkiem że będzie się ją w ten sposób używać. Obsługi tras i komponenty serwerowe już działają na serwerze, znajdują się pomiędzy klientem a backendem i mogą wysyłać kilka równoległych żądań oraz przekształcać łączny wynik, zanim ten dotrze do przeglądarki. Wiele zespołów, które przyjęły App Router, już realizuje zadania w stylu BFF, nie nazywając tego formalnie ani nie stosując tego podejścia w sposób spójny.

// 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),
  });
}

Załóżmy teraz, że aplikacja mobilna bezpośrednio łączy się z tymi samymi trzema usługami i sama przetwarza dane w języku Swift lub Kotlin, niezależnie implementując logikę określania dostępnych opcji wysyłki. Właśnie w ten sposób pojawia się rozbieżność. Dwie zespoły, dwa języki programowania, dwie różne interpretacje tej samej zasady biznesowej – prędzej czy później te interpretacje się różnią, a nikt tego nie zauważa, dopóki na biurko kogoś nie trafi zgłoszenie klienta.

Gdzie sprawa staje się polityczna, a nie tylko techniczna

Najtrudniejsza część prawidłowego wdrożenia BFF ma niewiele wspólnego z pisaniem kodu. Chodzi o to, kto jest jego właścicielem. BFF stworzony wyłącznie dla zespołu webowego będzie działał tylko wtedy, gdy osobą odpowiedzialną jest ktoś z uprawnieniami do szybkiej modyfikacji, najlepiej sam zespół webowy, a nie grupa na platformie wspólnej, skrępowana dwutygodniowym harmonogramem wydawania. Jeśli warstwa BFF znajdzie się w rękach odrębnego zespołu backendowego, który traktuje ją jak kolejny mikrosługę podlegającą takiemu samemu procesowi zatwierdzania jak wszystko inne, to nie rozwiązaliście pierwotnego problemu – po prostu przenieśliście go o krok bliżej klienta.

To także powód, dla którego ten wzorzec tak naturalnie pasuje do kodbazy Next.js: deweloperzy piszący obsługę tras są zazwyczaj tymi samymi osobami, które tworzą komponenty je wywołujące. Nie ma potrzeby składania zgłoszeń, nie ma przekazywania zadań między zespołami ani oddzielnego procesu wdrażania do skoordynowania. Jeśli potrzebujesz zmienić strukturę odpowiedzi z podsumowaniem koszyka zakupowego, dokonujesz tej zmiany w tym samym pull requestu co komponent, który ją wyświetla.

Pułapka: Przekształcanie każdej obsługi trasy w obiekt-boga

Istnieje inny, rzadko omawiany sposób awarii na drugim końcu spektrum. Gdy zespół odkryje, że obsługi tras mogą agregować cokolwiek zechcą, trudno oprzeć się pokusie stworzenia jednego ogromnego punktu końcowego, który przy pojedynczym wywołaniu zwróci cały początkowy stan aplikacji, ponieważ mniej ruchów w obie strony wydaje się oczywistym zwycięstwem. Czasami tak jest. Ale oznacza to również, że jeden obsługa trasy musi jednocześnie wspierać sześć niepowiązanych ze sobą funkcji, a mała modyfikacja modułu wysyłki zmusza do umieszczenia jej w tym samym pliku co widget punktów lojalnościowych, ponieważ oba zostały wtrącone do czegoś takiego jak getInitialAppState.

To podejście jest skuteczne, gdy endpoint BFF odpowiada jednemu ekranowi lub jednej dobrze zdefiniowanej funkcji, a nie „wszystkiemu, czego aplikacja mogłaby potrzebować przy załadunku”. Jeśli masz trudności z nazwaniem endpointu na podstawie konkretnej części interfejsu użytkownika, to zazwyczaj oznacza, że stał się zbyt szeroki.

Istnieje jeszcze jeden powiązany sygnał ostrzegawczy, na który warto zwrócić uwagę: punkty końcowe BFF, które przestają jedynie przekształcać dane i zaczynają na nowo implementować logikę biznesową. Obsługa trasy, która sama decyduje, czy dany klient kwalifikuje się do bezpłatnej dostawy, zamiast po prostu zapytać usługę cenową i przekazać odpowiedź, w ten sposób staje się drugim źródłem prawdy dla reguły, która już istnieje gdzie indziej w systemie. Zadaniem BFF jest tłumaczenie i łączenie danych, a nie podejmowanie decyzji. Gdy tylko zaczyna ono podejmować decyzje, wracamy do tego samego problemu dewiacji omawianego wcześniej, tylko teraz znajdującego się o jeden poziom bliżej klienta, gdzie jest znacznie trudniej go zauważyć podczas przeglądu kodu.

Mój punkt widzenia na ten temat

BFF nie musi być traktowany jako jakiś formalny zobowiązanie architektoniczne wraz z diagramem i stroną wiki, chociaż nie ma nic złego w takim podejściu, jeśli pasuje do sposobu funkcjonowania twojej organizacji. Dla większości zespołów już korzystających z Next.js przyjęcie tego podejścia nie polega tyle na wprowadzaniu czegoś nowego, ile na nazwaniu tego, co już istnieje: uznanie, że obsługi tras funkcjonują jako BFF, porzucenie pozorów, że są to zwykłe trasy API, oraz ustrukturyzowanie ich wokół tego, czego faktycznie potrzebuje klient, a nie wokół granic usług backendowych. Sam wzorzec nigdy nie był trudny. Trudne jest przyznanie się, że logika agregacji wymaga odpowiedniego miejsca, a następnie przekazanie go zespołowi frontendowemu, który faktycznie jest za nią odpowiedzialny, zamiast czekać, aż ekran zakupów będzie wymagał sześciu oddzielnych wywołań do renderowania, zanim ktoś zada pytanie o powód.

Pozycje pokrewne

  • Tworzenie interfejsów użytkownika dla agentów AI wieloetapowych za pomocą Next.js i AI SDK — Dowiedz się, jak zaprojektować interfejs agenta AI gotowy do użycia w produkcji, wykorzystując narzędzia typowane, pętle wieloetapowe oraz komponenty interfejsu generatywnego w formie strumieniowania w Next.js.
  • Automatyczne generowanie bezpiecznego pod względem typów klienta API dla Next.js z NestJS Swagger — Dowiedz się, jak usunąć duplikaty typów API poprzez użycie NestJS Swagger i Orval do automatycznego generowania bezpiecznych pod względem typów hooki React Query dla Next.js.