Strona główna / Artykuły / Przenoszenie API Express do obsługi tras w Next.js App Router

Artykuł opublikowany po angielsku.

Next.jsExpressReactKomponenty serweraMigracjaNode.js

Przenoszenie API Express do obsługi tras w Next.js App Router

Dowiedz się, jak przekształcić ścieżki Express, middleware oraz wzorce danych na Next.js App Router z użyciem komponentów serwerowych oraz jakie są kwestie związane z wdrażaniem.

3059 słów

Kiedy migracja na Next.js faktycznie ma sens (a kiedy nie)

App Router w Next.js staje się rozsądnym wyborem, gdy twój projekt spełnia co najmniej dwa z następujących warunków:

  • Szybkość początkowego ładowania lub widoczność w wyszukiwarkach jest kluczowa, a obecnie używasz aplikacji jednostronicowej, która pobiera dane z backendu Express po załadowaniu strony.
  • Twój proces wdrażania jest już skierowany na Vercel lub podobne platformy albo jesteś gotów przyjąć taką infrastrukturę — funkcje App Routera oparte na chmurze i bezserwerowym modelu zakładają takie środowisko hostowania.
  • Twoja aplikacja Express służy głównie do wyświetlania stron i obsługi standardowych operacji CRUD, zamiast zarządzać trwałymi połączeniami, zadaniami w tle lub procesami wymagającymi dużych obliczeń.
  • Pieczęć waszego zespołu inżynieryjnego polega na dogłębnym zrozumieniu Server Components. Ten framework nie polega po prostu na dodaniu routingu opartego na plikach do React. Architektura renderowania jest zasadniczo inna, a stosowanie starych wzorców aplikacji jednostronicowych spowoduje, że aplikacja będzie działać gorzej i bardziej mylić programistów niż oryginał.
  • Migracja nie jest zalecana w następujących sytuacjach:

    • Zależysz w dużej mierze od ugruntowanego ekosystemu middleware Express — specjalistycznych dostawców autoryzacji, bibliotek do ograniczania liczby żądań czy integracji z narzędziami do monitorowania — które wymagałyby całkowitej ponownej implementacji przy znikomych korzyściach.

    Zalecane podejście: zachowaj swoją API Express jako autorytatywne źródło danych i reguł biznesowych, a następnie wykorzystaj Next.js jako warstwę renderowania oraz backendu dla frontendu. Najpierw przenieś interfejs użytkownika oraz trasy służące do odczytu danych, pozostawiając bez zmian usługi wewnętrzne wymagające intensywnego zapisu. Ten przewodnik opiera się na tej strategii migracji.

    Mapowanie tras Express do obsługujących ich funkcji w Next.js

    Funkcje obsługujące trasy znajdujące się w pliku app/api/**/route.ts stanowią najbardziej bezpośredni zamiennik definicji tras w Express. Koncepcyjna różnica polega na tym, że zamiast obiektów req i res otrzymujesz obiekt Request i zwracasz obiekt Response (lub używasz NextResponse dla większej wygody), a każda metoda HTTP staje się oddzielną funkcją eksportowaną, zamiast wywołaniem metody router.get().

    Rozważmy standardową trasę Express, która pobiera i tworzy zamówienia:

    // express: routes/orders.ts
    import ⟨0⟩ from "express";
    import ⟨1⟩ from "../db";
    import ⟨2⟩ from "../middleware/auth";
    
    const router = Router(); router.get("/api/orders", requireAuth, async (req, res) => {
     const userId = req.user.id;
     const orders = await db.order.findMany({ where: ⟨3⟩ });
     res.json(⟪4⟩);
    }); router.post("/api/orders", requireAuth, async (req, res) => {
     const ⟨5⟩ = req.body;
     if (!items?.length) {
     return res.status(400).json(⟪6⟩);
     }
     const order = await db.order.create({
     data: ⟨7⟩,
     });
     res.status(201).json(⟪8⟩);
    }); export default router;
    

    Odpowiednia implementacja obsługi trasy:

    // app/api/orders/route.ts
    import ⟨0⟩ from "next/server";
    import ⟨1⟩ from "@/lib/db";
    import ⟨2⟩ from "@/lib/auth";
    
    export async function GET(req: NextRequest) {
     const user = await getSessionUser(req);
     if (!user) {
     return NextResponse.json(⟪3⟩, ⟨4⟩);
     } const orders = await db.order.findMany({ where: ⟨5⟩ });
     return NextResponse.json(⟪6⟩);
    }export async function POST(req: NextRequest) {
     const user = await getSessionUser(req);
     if (!user) {
     return NextResponse.json(⟪7⟩, ⟨8⟩);
     } const body = await req.json();
     if (!body.items?.length) {
     return NextResponse.json(⟪9⟩, { status: 4
    00 }); } const order = await db.order.create({ data: { userId: user.id, items: body.items }, }); return NextResponse.json({ order }, { status: 201 }); }

    Przy tłumaczeniu wzorców Express na obsługiwacze tras należy zwrócić uwagę na kilka szczegółów:

    Segментy dynamiczne wykorzystują składnię opartą na folderach zamiast przedrostków w postaci kropek. Trasa Express o adreście /api/orders/:id staje się app/api/orders/[id]/route.ts. Parametr jest przekazywany jako drugi argument funkcji obsługiwacza: GET(req, { params }: { params: Promise<{ id: string }> }). W aktualnych wersjach Next.js parametry są dostarczane jako obietnica, więc należy na nie poczekać przed odczytaniem ich wartości.

    Brak łańcucha middleware dla danej trasy. Middleware requireAuth z Express przekształca się albo w wspólną funkcję pomocniczą wywoływaną na początku każdego obsługiwacza (jak pokazano w powyższym przykładzie), albo – co jest lepsze – w logikę umieszczoną w pliku middleware.ts (omówione to w późniejszej sekcji), dzięki czemu poszczególne obsługiwacze tras nie muszą zajmować się kwestiami autoryzacji.

    Aparatowanie ciała żądania wymaga wyraźnych wywołań – należy napisać await req.json(), zamiast polegać na express.json(). Nie ma żadnego automatycznego aparatowania, co w rzeczywistości poprawia przejrzystość: unika się nieoczekiwanych ograniczeń wielkości ciała żądania narzucanych przez globalny middleware, który skonfigurowano miesiące temu i o którym zapomniano.

    Obsługi tras pozostają zwykłymi funkcjami Node lub Edge. Jeśli walidowałeś dane wejściowe za pomocą zod w swoich trasach Express, ten kod walidacyjny zostaje przeniesiony bez żadnych zmian.

    Komponenty serwera a istniejące wzorce renderowania klienta w React

    Aspekt ten najczęściej zaskakuje zespoły bardziej niż jakikolwiek inny. W architekturze Express plus React każdy komponent domyślnie jest komponentem klienta: renderuje się w przeglądarce, a gdy potrzebuje danych, wywołuje twoją API z pomocą useEffect lub za pośrednictwem biblioteki do pobierania danych, takiej jak React Query.

    // stary wzorzec: React renderowany po stronie klienta, który komunikuje się z Express
    function OrderList() {
     const [orders, setOrders] = useState<Order[] | null>(null);
    
     useEffect(() => {
     fetch("/api/orders", { credentials: "include" })
     .then((r) => r.json())
     .then((data) => setOrders(data.orders));
     }, []); if (!orders) return <Spinner />;
     return (
     <ul>
     {orders.map((o) => (
     <li key={o.id}>{o.id} — ${o.total}</li>
     ))}
     </ul>
     );
    }
    

    App Router odwraca tę domyślną zasadę. Każdy komponent jest komponentem serwerowym, chyba że wyraźnie zaznaczysz inaczej, co oznacza, że jest wykonywany na serwerze, bezpośrednio uzyskuje dostęp do bazy danych lub usług i nigdy nie wysyła swojego JavaScripta na stronę klienta. Dla danych należących do danej strony można całkowicie pominąć trasę API.

    i zapytaj o to bezpośrednio:

    // app/orders/page.tsx — Komponent serwerowy, bez „use client”
    import { db } from "@/lib/db";
    import { getSessionUser } from "@/lib/auth";
    import { redirect } from "next/navigation";
    
    export default async function OrdersPage() {
     const user = await getSessionUser();
     if (!user) redirect("/login"); // bezpośredni dostęp do bazy danych, bez fetch, bez stanu ładowania, bez kosztów związanych z paczką kliencką
     const orders = await db.order.findMany({
     where: { userId: user.id }
    rver dostarcza HTML, który już zawiera dane. Dodaj "use client" tylko wtedy, gdy komponent wymaga interaktywności: stanu, efektów, obsługi zdarzeń lub API dostępnych wyłącznie w przeglądarce:

    // app/orders/OrderFilters.tsx
    "use client";
    
    import > from "react";
    import > from "next/navigation"; export function OrderFilters() {
     const router = useRouter();
     const params = useSearchParams();
     const [status, setStatus] = useState(params.get("status") ?? "all"); function apply(next: string) {
     setStatus(next);
     const url = new URLSearchParams(params);
     url.set("status", next);
     router.push(`/orders?$>`);
     } return (
     <select value=> onChange=>>
     <option value="all">Wszystko</option>
     <option value="pending">W oczekiwaniu</option>
     <option value="shipped">Wysłane</option>
     </select>
     );
    }
    
    /code>

    Praktyczna zasada dla zespołów: włączaj "use client" tak nisko w drzewie komponentów, jak to możliwe. Zachowaj swoje strony i układy jako komponenty serwerowe; stosuj tę instrukcję tylko w przypadku komponentów liściowych, które rzeczywiście wymagają interaktywności. Jeśli z przyzwyczajenia dodasz "use client" do każdego komponentu (co zdarza się podczas przenoszenia aplikacji jednostronicowej bez ponownego przemyślenia architektury), nie uzyskasz żadnych korzyści z App Routera i ostatecznie będziesz mieć gorszy model mentalny niż przy oryginalnym ustawieniu.

    Zastępowanie środowiska autoryzacji Express środowiskiem Next.js

    W Express środowisko autoryzacji jest uruchamiane dla każdej trasy w procesie Node.js. Next.js dostarcza plik middleware.ts, który przechwytuje żądania na warstwie brzegowej, zanim dotrą do jakiejkolwiek strony lub obsługiwanego elementu, pełniąc rolę odpowiednika architektonicznego:

    // stare: middleware/auth.ts (Express)
    import jwt from „jsonwebtoken“;
    
    export function requireAuth(req, res, next) {
     const token = req.cookies.session;
     if (!token) return res.status(401).json([„0“]); try {
     req.user = jwt.verify(token, process.env.JWT_SECRET!);
     next();
     } catch {
     res.status(401).json([„1“]);
     }
    }
    
    // middleware.ts — znajduje się w korzeniu projektu
    import „2“ from „next/server“;
    import „3“ from „jose“; // kompatybilne z Edge, w odróżnieniu od jsonwebtoken
    
    const PROTECTED_PREFIXES = [„/dashboard“, „/orders“, „/api/orders“]; export async funkcio
    n middleware(req: NextRequest) { const isProtected = PROTECTED_PREFIXES.some((p) => req.nextUrl.pathname.startsWith(p) ); if (!isProtected) return NextResponse.next(); const token = req.cookies.get("session")?.value; if (!token) { return NextResponse.redirect(new URL("/login", req.url)); } try { const secret = new TextEncoder().encode(process.env.JWT_SECRET!); const { payload } = await jwtVerify(token, secret); // przekazanie zweryfikowanego ID użytkownika dalej poprzez nagłówek żądania const headers = new Headers(req.headers); headers.set("x-user-id", String(payload.sub)); return NextResponse.next({ request: { headers } }); } catch { return NextResponse.redirect(new URL("/login", req.url)); } }export const config = { matcher: ["/dashboard/:path*", "/orders/:path*", "/api/orders/:path*"], };

    Twoje

    Problemy, które regularnie utrudniają pracę zespołom migrującym:

    1. Domyślnie middleware działa w środowisku Edge runtime, a nie w Node.js. Każda biblioteka polegająca na modułach podstawowych Node.js — takie jak jsonwebtoken czy typowe klienty bazy danych — będzie działać niewłaściwie lub w ogóle przestać funkcjonować. Należy zastosować alternatywy przygotowane do pracy w środowisku Edge: jose jest powszechnie stosowanym wyborem do operacji z JWT. Wszystkie zapytania do bazy danych należy przenieść do Server Components lub Route Handlers, gdzie dostępne jest pełne środowisko Node.js; nigdy nie należy ich wykonywać w middleware.
  • Zapytania pełnej sesji bazy danych są niewykonalne w środowisku middleware, w odróżnieniu od wzorców Express, które mogą wykonywać SELECT * FROM sessions WHERE id = ?. Ogranicz logikę middleware do szybkich, bezstanowych operacji, takich jak weryfikacja podpisu tokena. Pytania dotyczące uprawnień — „czy ten użytkownik może przeglądać tę zamówienie?” — należy przenieść do komponentu strony lub samego obsługiwanego elementu, gdzie dostęp do bazy danych i API Node.js są w pełni dostępne.
  • Wzorce pobierania danych: Komponenty serwerowe kontra obecne podejście oparte na wywołaniach API

    Większość architektur Express w połączeniu z Reactem follows przewidywalną sekwencję: komponent jest montowany, pobiera dane z API, API wysyła zapytanie do bazy danych, JSON wraca do przeglądarki, a komponent się aktualizuje. Dwa ruchy w sieci — od klienta do serwera, od serwera do bazy danych — dostarczają informacje, które serwer już posiadał.

    Z App Routerem operacje odczytu w komponentach serwerowych sprowadzają je do jednego kroku: serwer zapytuje bazę danych i przesyła bezpośrednio do klienta strumień HTML zawierający wynik, jak pokazano we wcześniejszym kodzie OrdersPage. W przypadku operacji zapisu istnieją dwa typowe podejścia: Route Handlers, gdy potrzebujesz konwencjonalnej API (na przykład publicznej interfejsu REST), lub Server Actions do mutacji wywoływanych przez własne formularze i interfejs użytkownika.

    Server Actions najbardziej odbiegają od konwencji Express. Definiujesz funkcję, która jest wykonywana na serwerze, i uruchamiasz ją bezpośrednio z formularza, nie tworząc żadnej wyraźnej trasy API:

    // app/orders/actions.ts
    "use server";
    
    import { db } from "@/lib/db";
    import { getSessionUser } from "@/lib/auth";
    import { revalidatePath } from "next/cache";
    import { redirect } from "next/navigation";export async function createOrder(formData: FormData) {
     const user = await getSessionUser();
     if (!user) redirect("/login"); const itemId = formData.get("itemId");
     if (typeof itemId !== "string" || !itemId) {
     throw new Error("Niezbędny jest parametr itemId");
     } await db.order.create({
     data: { userId: user.id, items: [{ itemId, qty: 1 }] },
     }); // ponowne wyświetlenie strony z zamówieniami ze świeżymi danymi serwerowymi — nie ma potrzeby ponownego pobierania na stronie klienta
     revalidatePath("/orders");
    }
    
    // app/or
    ders/NewOrderForm.tsx import ⟨0⟩ from „./actions“;
    export function NewOrderForm() {
     return (
     <form action=⟪1⟩>
     <input type="text" name="itemId" placeholder="ID elementu" required />
     <button type="submit">Stworzyć zamówienie</button>
     </form>
     );
    }
    

    Wybór między działaniami serwera a obsługą tras:

    Działanie serwera to wewnętrzne wywołanie funkcji z interfejsu użytkownika do serwera; obsługa trasy to prawdziwy punkt końcowy HTTP. Różnica ta ma znaczenie przy decydowaniu, który wzorzec pasuje do danej mutyacji.

    Użyj Route Handlera, gdy endpoint musi być stabilny i dostępny z zewnątrz aplikacji Next.js – np. przez klienta mobilnego, integrację z podmiotem trzecim lub publiczną API. Route Handlery zapewniają wyraźne ścieżki URL, czasowniki HTTP oraz kontrakt, który sam kontrolujesz i wersjonujesz.

    Użyj Server Actiona, gdy zmiana pochodzi z twoich własnych form i komponentów interaktywnych. Server Actions wymagają mniej kodu szablonowego i automatycznie odnowiają dane z pamięci podręcznej za pomocą revalidatePath lub revalidateTag, eliminując konieczność ręcznego aktualizowania pamięci podręcznej po wywołaniu fetch. Nie są przeznaczone dla użytkowników zewnętrznych ani do zapewnienia zgodności wstecznej; traktuj je jako wewnętrzne zdalne wywołania procedur.

    Test praktyczny: jeśli chcesz udokumentować punkt końcowy dla kogoś spoza twojego zespołu, utwórz z niego Route Handler. Jeśli istnieje on wyłącznie w celu obsługi przycisku lub formularza w interfejsie użytkownika, prostsze będzie użycie Server Action.

    Różnice w implementacji (Vercel vs ECS/EB)

    Zespoły przyzwyczajone do wdrażania Express na ECS lub Elastic Beanstalk napotykają fundamentalną zmianę architektoniczną przy pracy z Next.js na Vercel. Aplikacja Express działa jako proces Node o długim czasie trwania: jeden proces obsługuje wiele żądań, utrzymuje aktywne połączenia z bazą danych i charakteryzuje się przewidywalnym zużyciem pamięci oraz czasem uruchamiania.

    Next.js na Vercel rozpakowuje strony oraz obsługę tras jako oddzielne funkcje serverless lub edge functions. Każda z tych funkcji uruchamia się niezależnie, działa w ramach własnych ograniczeń wykonywania i nie dzieli się trwałym pulą połączeń do bazy danych w taki sposób, jak to ma miejsce przy pojedynczym procesie Express. Jeśli utworzysz klienta Prisma w taki sam sposób, jak to robiono w Express, przy dużej obciążeniu wyczerpiesz limit połączeń do bazy danych, ponieważ każda wywołanie funkcji może utworzyć własne połączenie.

    // lib/db.ts — wymagany wzorzec dla serverless Prisma
    import { PrismaClient } from "@prisma/client";
    
    const globalForPrisma = global as unknown as { prisma: PrismaClient };// ponowne użycie klienta przy kolejnych wywołaniach zamiast tworzenia nowego za każdym razem
    export const db =
     globalForPrisma.prisma ??
     new PrismaClient({
     // użyj łącza typu pooled (np. PgBouncer / Prisma Accelerate / RDS Proxy)
     datasources: { db: { url: process.env.DATABASE_URL_POOLED } },
     });if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
    

    Wzorzec ten umożliwia ponowne użycie klienta przy kolejnych wywołaniach zamiast tworzenia nowej instancji za każdym razem.

    Jeśli zainstalujesz Next.js w ECS lub Elastic Beanstalk zamiast na Vercel, framework będzie działał jako tradycyjny serwer Node. Uruchom next build, a następnie next start, albo skonfiguruj tryb wyjściowy typu standalone, aby uzyskać lżejsze artefakty Dockera. Zachowujesz zalety długotrwałych procesów, ale tracisz automatyczną sieć edge Vercel, incrementalną regenerację treści statycznych w CDN oraz możliwość szybkich testów bez konfiguracji. Musisz sam skonfigurować te funkcje albo pogodzić się z ich brakiem. To jest uzasadniona decyzja inżynieryjna, a nie kompromis; wiele zespołów używa Next.js w ECS właśnie dlatego, że już posiada taką infrastrukturę i woli nie dzielić hostingu między dwoma dostawcami.

    Przydziel czas na dwa dodatkowe zadania migracyjne: zmiennice środowiskowe (Next.js wymaga przedrostka NEXT_PUBLIC_ dla każdej zmiennej dostępnej w przeglądarce — sprawdź każdą zmienną, którą przekazałeś do budowania aplikacji React) oraz konfigurację w czasie budowy versus w czasie wykonywania (wartości wbudowane w statyczny eksport zachowują się inaczej niż te odczytywane na żądanie, w przeciwieństwie do pojedynczego procesu Express, gdzie wszystko działa w czasie wykonywania).

    Lista kontrolna migracji i częste pułapki

    Wykonuj te kroki kolejno:

    1. Rozwijaj aplikację Next.js równolegle z serwerem Express — nie dotykaj istniejącej aplikacji podczas początkowej konfiguracji.
    2. Najpierw przekształć strony wymagające intensywnego odczytu i zoptymalizowane pod wyszukiwarki — te strony najbardziej skorzystają na użyciu Server Components i niosą minimalne ryzyko migracji.
  • Zaimplementuj autoryzację w pliku middleware.ts za pomocą biblioteki do weryfikacji tokenów kompatybilnej z środowiskiem Edge Runtime.
  • Zmień punkty końcowe typu GET na komponenty serwerowe, które pobierają dane bezpośrednio, gdy te dane służą wyłącznie twoim własnym stronom, unikając w ten sposób konieczności używania obsługowników tras (Route Handlers).
  • Zmień punkty końcowe typu mutation na działania serwerowe (Server Actions), jeśli odpowiadają one wyłącznie twojemu własnemu interfejsowi; zachowaj obsługowniki tras dla wszystkich punktów końcowych używanych przez zewnętrznych klientów.
  • Rozwiąż problem sposobu łączenia się z bazą danych w środowisku serverless przed migrowaniem jakichkolwiek operacji zapisu — ten krok zapobiega przerwom w działaniu systemu, a nie jest czymś, co można odłożyć.
  • Najpóźniej należy dostosować procesy wdrażania oraz pipeline’y ciągłej integracji, po tym jak aplikacja działa poprawnie zarówno w środowisku rozwojowym, jak i produkcyjnym (next build && next start) — next dev maskuje pewne błędy, które pojawiają się tylko w trybie produkcyjnym, takie jak naruszenia zasad działania między serwerem a klientem.
  • Częste błędy występujące podczas migracji:

    • Importowanie kodu dostępnego tylko na serwerze do komponentu klienta. Gdy komponent z instrukcją "use client" importuje cokolwiek, co ma dostęp do fs, klienta bazy danych lub tajemnic, proces budowania albo się nie powiedzie, albo — co jest bardziej niebezpieczne — tajemnice trafią do pliku JavaScript klienta. Należy zainstalować pakiet server-only, aby wywołać błąd podczas budowania, zamiast w tajemnicy ujawniać dane dostępowe.
  • Pomijanie revalidatePath lub revalidateTag po akcji serwera. Bez wyraźnej ponownej walidacji interfejs pokazuje przestarzałe dane po zmianie, ponieważ komponenty serwerowe mogą być w pamięci podręcznej.
  • Oznaczanie każdego komponentu tagiem "use client". Nawyki z aplikacji jednostronicowych są główną przyczyną, dla której aplikacja Next.js nie osiąga lepszych wyników niż oryginał.
  • Oczekiwanie, że middleware będzie obsługiwać całą powierzchnię API Node.js. Middleware działa domyślnie w środowisku Edge — projektuj swoje procedury autoryzacji z uwzględnieniem ograniczeń tego środowiska.
  • Pomijanie testów obciążeniowych zbiornika połączeń bazodanowych przed wdrożeniem w produkcji. To zaniedbanie powoduje wyświetlanie alertów w środku nocy.
  • Podsumowanie

    Jeśli nadal jesteś niepewny, zacznij od prostego eksperymentu: wybierz jedną stronę wymagającą dużo odczytów i istotną pod kątem SEO z obecnej aplikacji, przerób ją na komponent serwerowy, który będzie wysyłał zapytania do bazy danych lub bezpośrednio wywoływał istniejące API Express, a następnie umieść go na trasie, której nie obsługuje serwer Express. Porównaj czas dostarczenia pierwszego bajtu oraz rozmiar pliku przed migrowaniem kolejnych stron. Po zweryfikowaniu tej metody przenieś logikę autoryzacji do pliku middleware.ts, a następnie stopniowo przekształcaj najbardziej obciążone procesy w Server Actions – takie stopniowe migrowanie jest bezpieczniejsze i bardziej praktyczne niż całkowite przepisanie kodu. Zespoły, które napotykają problemy, to te, które migrują całą bazę kodu, zanim określą, gdzie komponenty serwerowe faktycznie zmniejszają pracę, a gdzie jedynie wprowadzają nową warstwę koncepcyjną na istniejącym systemie.

    Literatura pokrewna

  • Zastąpienie Jest narodowym wykonywaczem testów w Node 24 — Przykład z praktyki pokazuje, jak wbudowany wykonywacz testów w Node 24 oraz natywna obsługa TypeScript skracają czas CI, jednocześnie eliminując cztery zależności.
  • Dostosowywanie nowych funkcji zarządzania kawałkami w Turbopack w Next.js 16.3 — Praktyczny przegląd nowej konfiguracji turbopackChunking w Next.js 16.3, wyjaśniający, jak maxChunkCountPerGroup i generateComponentChunks wpływają na rozmiar pliku pakietu oraz funkcjonowanie cache’u.
  • Przekształcanie obsługujących trasy w Next.js w celową warstwę BFF — Dowiedz się, co rozwiązuje wzorzec Backend for Frontend, dlaczego pojawia się ponownie w aplikacjach Next.js oraz jak uniknąć przekształcania obsługujących tras w nadmiernie złożone obiekty.