Route Handlers czy Server Actions: najkrótsza odpowiedź
Wybierz Route Handler w sytuacji, kiedy operacja wymaga dedykowanego punktu końcowego (stałego adresu URL, konkretnej metody HTTP, własnych nagłówków i statusów czy specyficznego formatu odpowiedzi) i ma być konsumowana przez zewnętrzne systemy lub klientów spoza interfejsu Next.js. Kiedy jednak mutacja służy do obsługi formularzy bądź bezpośrednich interakcji w komponencie Reacta i jej celem jest natychmiastowe odświeżenie stanu UI, właściwym wyborem będzie Server Action.
| Pytanie | Wybór |
|---|---|
| Czy klientem jest aplikacja mobilna, webhook lub inny serwis? | Route Handler |
| Czy potrzebujesz GET, pliku, SSE, CORS albo własnych nagłówków? | Route Handler |
| Czy operacja wynika bezpośrednio z formularza lub przycisku Reacta? | Server Action |
Czy potrzebujesz useActionState, useOptimistic albo progresywnego ulepszania? | Server Action |
| Czy Server Component tylko pobiera dane? | Bezpośrednie wywołanie serwisu lub bazy |
Warto pamiętać, że w dokumentacji Reacta pojęciem nadrzędnym jest Server Function. Z kolei Server Action to po prostu taka funkcja serwerowa, którą osadzono bezpośrednio w kontekście mutacji, na przykład przekazując ją do atrybutu action lub formAction albo wywołując wewnątrz startTransition.
Wspólna logika biznesowa zamiast dwóch implementacji
Zarówno Route Handler, jak i Server Action pełnią wyłącznie rolę adapterów wejściowych. Żaden z tych mechanizmów nie powinien samodzielnie implementować logiki biznesowej, takiej jak kalkulacja cen, weryfikacja uprawnień czy bezpośredni zapis produktu do bazy. Cała ta logika powinna spoczywać we wspólnym serwisie domenowym, który operuje na danych całkowicie odseparowanych od warstwy transportowej, bez zależności od obiektów Request, Response czy struktury FormData.
Choć wstępna walidacja danych wejściowych powinna odbywać się na samej granicy warstwy transportowej, serwis domenowy nadal oczekuje ścisłego, typowanego modelu i samodzielnie egzekwuje reguły niezależne od użytego adaptera. Wprowadzenie dedykowanej klasy wyjątku, np. ForbiddenError, pozwala jednoznacznie zmapować odmowę dostępu na status HTTP 403 w Route Handlerze lub odpowiedni komunikat w Server Action, bez ryzyka pomylenia błędu biznesowego z awarią infrastruktury.
Route Handlers: jawny kontrakt HTTP
Route Handler definiuje się w pliku route.ts wewnątrz katalogu app/, opierając jego działanie na natywnych interfejsach Web API: Request oraz Response. Next.js automatycznie rozpoznaje i obsługuje w tym pliku eksportowane metody HTTP: GET, POST, PUT, PATCH, DELETE, HEAD oraz OPTIONS.
Ponieważ powyższy przykład koncentruje się na samej granicy warstwy HTTP, funkcja requireApiActor() pełni rolę pomocniczą i odpowiada za weryfikację tokenu lub aktywnej sesji. Przewidywalny błąd domenowy jest w tym miejscu jawnie mapowany na status 403 Forbidden, natomiast nieoczekiwany wyjątek zostaje zarejestrowany w logach i zwrócony jako ogólny błąd 500 Internal Server Error, bez ujawniania jakichkolwiek szczegółów technicznych klientowi.
Kiedy Route Handler jest właściwy?
- Publiczne lub partnerskie API
- Aplikacja mobilna albo inny frontend
- Webhook, OAuth callback i integracja zewnętrzna
- Download, XML, obraz, RSS lub inny format niż UI Reacta
- CORS, własne statusy, nagłówki i wersjonowany kontrakt
- Streaming HTTP, w tym Server-Sent Events
Route Handler może pełnić funkcję wzorca Backend-for-Frontend w sytuacjach, gdy przeglądarka faktycznie potrzebuje tradycyjnego kontraktu HTTP, na przykład przy cyklicznym odpytywaniu endpointu (polling) za pomocą bibliotek takich jak SWR czy TanStack Query. Nie należy go jednak tworzyć wyłącznie po to, aby komponent serwerowy wywoływał fetch() do własnego punktu końcowego /api. Jeśli taki komponent jest prerenderowany na etapie budowania, fetch() zakończy build błędem, bo nie działa wtedy jeszcze żaden serwer HTTP. Z kolei przy renderowaniu na żądanie wprowadzamy w ten sposób całkowicie zbędny skok sieciowy i narzut opóźnienia.
Webhook wymaga czegoś więcej niż weryfikacji podpisu
Punkt końcowy webhooka powinien w pierwszej kolejności odczytać surowe body (np. przez await request.text()), zweryfikować podpis kryptograficzny dostawcy i zarejestrować zdarzenie wraz z jego unikalnym identyfikatorem. Należy jednak pamiętać, że samo sprawdzenie obecności zdarzenia w tabeli processed_events nie gwarantuje semantyki exactly-once. Jeśli awaria systemu nastąpi tuż po wykonaniu efektu ubocznego w domenie, ale przed zapisaniem statusu completed, ponowione żądanie od dostawcy doprowadzi do powstania duplikatu.
Znacznie bezpieczniejszym wzorcem jest natychmiastowy zapis napływającego zdarzenia do transakcyjnego inboxa, szybkie zwrócenie odpowiedzi ze statusem 2xx i oddelegowanie właściwego przetwarzania do asynchronicznej kolejki z idempotentnym handlerem domenowym. Jest to kluczowe z dwóch powodów: dostawcy zewnętrzni mogą wysyłać zdarzenia wielokrotnie oraz w nieregularnej kolejności, a długotrwałe wykonywanie logiki biznesowej bezpośrednio wewnątrz Route Handlera zwiększa ryzyko przekroczenia limitu czasu (timeout) na platformach serverless.
Server Actions: mutacje sprzężone z Reactem
Server Action jest asynchroniczną funkcją serwerową wywoływaną żądaniem HTTP POST. Nie należy jej jednak projektować jako publicznego, stabilnego punktu końcowego dla zewnętrznych klientów. React przesyła argumenty do tej funkcji za pomocą własnego, dedykowanego protokołu, a Next.js potrafi w ramach pojedynczej odpowiedzi serwera zwrócić zarówno wynik wykonanej mutacji, jak i zaktualizowane drzewo komponentów UI.
Użycie liczby jednostek minimalnych w warstwie domenowej ogranicza problemy z arytmetyką zmiennoprzecinkową. Formularz może przyjmować wartość dziesiętną, ale przed zapisem przelicza ją na całkowite priceInCents.
Formularz osadzony w komponencie serwerowym zostanie wysłany nawet przy wyłączonym JavaScripcie w przeglądarce. Nie pokaże jednak stanu oczekiwania, a po wysłaniu przeglądarka przeładuje stronę. Z kolei formularz w komponencie klienckim wysłany przed zakończeniem hydratacji zostaje zakolejkowany, a React w pierwszej kolejności hydratuje właśnie ten formularz. Natomiast wywołanie Server Action z onClick czy useEffect wymaga już aktywnego JavaScriptu i powinno być zawsze opakowane w startTransition. Jeśli zależy nam na natychmiastowej reakcji interfejsu, możemy zastosować hook useOptimistic, pamiętając jednak, że optymistyczna zmiana widoku jedynie maskuje opóźnienie sieciowe i w żaden sposób nie zastępuje walidacji ani potwierdzenia zapisu przez serwer.
Kiedy Server Action jest właściwa?
- Formularz tworzący, edytujący lub usuwający dane
- Mutacja wywołana przez przycisk lub interakcję Reacta
Stan błędu i oczekiwania obsługiwany przez
useActionState- Optymistyczna aktualizacja przez
useOptimistic - Odświeżenie UI i cache po mutacji
Porównanie Route Handlers i Server Actions
| Cecha | Route Handlers | Server Actions |
|---|---|---|
| Kontrakt | URL, metoda, status, nagłówki i body | Wewnętrzny protokół React/Next.js |
| Metody | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS | POST |
| Zewnętrzni konsumenci | Tak | Nie jako wspierany kontrakt |
| Formularze React | Ręczna obsługa | Natywne action i formAction |
| Oczekiwane błędy | Status HTTP i body | Zwracany stan, np. useActionState |
| Rewalidacja | revalidatePath, revalidateTag | Dodatkowo updateTag i odświeżenie UI |
SSE i kontrola Response | Tak | Nie |
| Zaktualizowane UI RSC w odpowiedzi | Nie dotyczy | Tak, po revalidatePath, updateTag, refresh lub redirect |
| Progresywne ulepszanie | Zależy od klienta | Tak dla formularzy |
| Type safety klient-serwer | Wymaga schematu lub klienta API | Wynika z importowanej funkcji, ale runtime nadal wymaga walidacji |
Bezpieczeństwo: oba mechanizmy są publicznym wejściem
Dyrektywa 'use server' nie oznacza prywatności. Argumenty Server Action są kontrolowane przez klienta, a wyeksportowana akcja jest osiągalna przez żądanie POST. Next.js używa niedeterministycznych identyfikatorów, usuwa nieużywane akcje i porównuje Origin z Host albo X-Forwarded-Host, ale te zabezpieczenia nie zastępują autoryzacji.
Pamiętaj, żeby sprawdzać nie tylko to, czy użytkownik jest zalogowany, ale czy może zmienić konkretny rekord. Dla operacji na zasobie preferuj atomowy warunek, na przykład deleteMany({ where: { id, ownerId: actor.userId } }), zamiast osobnego odczytu i późniejszego usunięcia podatnego na wyścig. Route Handlery nie dostają automatycznie identycznego modelu ochrony CSRF. Mutacje oparte na cookie sesyjnym powinny weryfikować origin lub token CSRF. Publiczne API wymaga również świadomego CORS, rate limitingu, limitów body i jednoznacznego uwierzytelnienia. Nie przekazuj też do klienta surowych wyjątków ani nadmiarowych pól z bazy.
Cache, rewalidacja i limity
Route Handlery nie są domyślnie cache'owane. Bez Cache Components możesz jawnie ustawić dynamic = 'force-static' dla GET. Przy włączonych Cache Components deterministyczny handler może zostać prerenderowany. Dane z bazy lub API cache'uj w wydzielonej funkcji z dyrektywą 'use cache', bo dyrektywy nie można umieścić bezpośrednio w ciele Route Handlera. Do takiej funkcji nie trafiają dane z żądania, np. cookies() czy headers(). revalidatePath i revalidateTag działają w obu mechanizmach. Wywołanie revalidatePath z Route Handlera oznacza ścieżkę do odświeżenia przy kolejnej wizycie. W Server Action może dodatkowo od razu zaktualizować oglądany interfejs. updateTag, zapewniający scenariusz read-your-own-writes, jest dostępny tylko w Server Actions.
Pamiętaj, że argumenty oraz wynik Server Function muszą należeć do typów serializowalnych przez React. Domyślny limit body Server Actions wynosi 1 MB i można go zmienić opcją experimental.serverActions.bodySizeLimit w next.config, ale duże pliki lepiej wysyłać bezpośrednio do storage. Route Handler daje kontrolę nad Request, Response, nagłówkami i streamingiem, jednak nadal podlega limitom platformy.
Błędy i obserwowalność
W architekturze Route Handlera każdy przewidywalny błąd należy jawnie zmapować na odpowiedni kod statusu HTTP oraz spójny format odpowiedzi.
- Nieprawidłowa składnia JSON-a — 400 Bad Request.
Błąd walidacji schematu — 422 Unprocessable Content.
- Brak aktywnej sesji — 401 Unauthorized.
- Brak wystarczających uprawnień — 403 Forbidden.
Nieobsłużony wyjątek — ogólny błąd 500 Internal Server Error, z unikalnym identyfikatorem zdarzenia (correlation ID) dołączonym do odpowiedzi lub logów, co ułatwia późniejsze debugowanie.
W przypadku Server Actions przewidywalne problemy, takie jak błędy walidacji formularza, należy zwracać bezpośrednio jako obiekty stanu obsługiwane przez hook useActionState. Rzucanie wyjątków warto zarezerwować wyłącznie dla krytycznych, nieobsłużonych awarii, które powinny zostać przechwycone przez najbliższy komponent error.tsx. Po pomyślnym wykonaniu operacji funkcja może zwrócić zaktualizowany stan interfejsu lub przekierować użytkownika za pomocą redirect(). Ten podział odpowiedzialności jest ważniejszy niż sama różnica składni między fetch() a bezpośrednim wywołaniem funkcji.


