Route Handlers czy Server Actions? Kiedy co wybrać
Wybór między Route Handlers a Server Actions w Next.js to kwestia bezpieczeństwa i architektury. Poznaj różnice i podejmij świadomą decyzję.
Maciej Sala
Founder StriveLab
6 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 13 lipca 2026)
Dwa sposoby na logikę serwerową w App Routerze
Jak pisałem wyżej, nie należy ich traktować naprzemiennie, ponieważ Route Handlers to klasyczne endpointy HTTP, a Server Actions to funkcje serwerowe wołane prosto z komponentów React. Ta różnica pociąga za sobą decyzje o architekturze, bezpieczeństwie, a także wydajności.
W ramach drobnego doprecyzowania: w dokumentacji React spotkasz dziś nazwę Server Functions, a określenie Server Actions jest używane wtedy, gdy te funkcje obsługują akcje i mutacje, najczęściej przez formularze lub interakcje w UI.
Route Handlers w Next.js jako endpointy API
Route Handlers to następca API Routes z Pages Routera. Tworzysz plik route.ts w katalogu app/ i eksportujesz funkcje odpowiadające metodom HTTP. Do walidacji danych wejściowych w przykładzie używam , bo sam TypeScript nie sprawdza treści JSON-a w runtime.
Zewnętrzni konsumenci. Mobilna aplikacja, inny serwis, webhook,
zewnętrzne integracje
Publiczne API. Endpointy dostępne bez UI Next.js
Webhooks. Stripe, CMS, płatności
REST/GraphQL — standardowe interfejsy API
Streaming. i
długie odpowiedzi
Custom auth flow. OAuth callbacks oraz token refresh
Kontrakt HTTP dla własnego frontendu — download pliku,
, specjalne nagłówki, statusy i odpowiedzi
konsumowane przez fetch
Route Handler nie jest przeznaczony wyłącznie do roli publicznego API dla obcych firm, ponieważ może z powodzeniem pełnić funkcję Backend for Frontend (BFF), w sytuacji gdy przeglądarka rzeczywiście potrzebuje kontraktu HTTP. Trzeba pamiętać jednak, by nie tworzyć go tylko po to, by Server Component w tej samej aplikacji wykonywał fetch('/api/products'), bo to zbędny skok HTTP. Zamiast tego, kod serwerowy powinien bezpośrednio wywoływać wspólną funkcję lub operować na bazie danych.
Code
// app/api/webhooks/stripe/route.tsimport { headers } from 'next/headers'import Stripe from 'stripe'const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)export async function POST(request: Request) { const body = await request.text() const headersList = await headers() const signature = headersList.get('stripe-signature') if (!signature) { return new Response('Missing signature', { status: 400 }) } let event: Stripe.Event try { event = stripe.webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET!, ) } catch { return new Response('Invalid signature', { status: 400 }) } try { // Tabela inbox ma unikalny indeks na event.id i stan przetwarzania. const shouldProcess = await claimWebhookEvent(event.id) if (!shouldProcess) return new Response('Already processed', { status: 200 }) switch (event.type) { case 'checkout.session.completed': await handleCheckoutComplete(event.data.object) break case 'invoice.payment_failed': await handlePaymentFailed(event.data.object) break } await markWebhookCompleted(event.id) return new Response('OK', { status: 200 }) } catch { await markWebhookFailed(event.id) // Błąd serwera powinien umożliwić dostawcy ponowienie webhooka. return new Response('Webhook processing failed', { status: 500 }) }}
Webhooki są dostarczane co najmniej raz i mogą przyjść ponownie lub poza kolejnością. Weryfikacja podpisu jest obowiązkowa, ale sama w sobie nie zapewnia idempotencji. Identyfikator zdarzenia należy zapisywać z unikalnym ograniczeniem oraz stanem processing/completed/failed, a operacje bazodanowe wykonywać transakcyjnie. Ważne jest, aby nie oznaczać zdarzenia jako zakończonego przed osiągnięciem efektu biznesowego, ponieważ nieudana pierwsza próba mogłaby zablokować ponowienie. Ponadto, surowe request.text() musi zostać odczytane przed parsowaniem JSON, gdyż podpis obejmuje dokładne bajty treści.
Server Actions jako funkcje serwerowe w komponentach
Server Actions to funkcje oznaczone dyrektywą "use server", które wywołujesz bezpośrednio z formularzy HTML lub kodu klienta i to bez ręcznego tworzenia endpointów API oraz fetch requestów.
Route Handlers vs Server Actions: bezpośrednie porównanie
Cecha
Route Handlers
Server Actions
Typ
Endpoint HTTP (REST)
Funkcja RPC
Wywołanie
fetch('https://api.example.com/...')
Bezpośrednie z formularza/kodu
Metody HTTP
GET, POST, PUT, DELETE, PATCH
Tylko POST (wewnętrznie)
Kontrakt
Stabilny URL, status, nagłówki, body
Wewnętrzny protokół React/Next
Formularze
Ręczna obsługa
Natywna integracja
Rewalidacja
Jawna; revalidatePath/revalidateTag
Jawna; dodatkowo updateTag
Streaming
Tak (SSE, chunked)
Nie
Progresywne ulepszanie
Zależy od klienta/formularza
Tak dla formularzy
Zewnętrzni konsumenci
Tak
Nie
Bezpieczeństwo Route Handlers i Server Actions
Route Handlers i samodzielna autoryzacja
Code
// app/api/admin/users/route.tsimport { auth } from '@/lib/auth'import { db } from '@/lib/db'export async function GET() { const session = await auth() if (!session || session.user.role !== 'admin') { return new Response('Forbidden', { status: 403 }) } const users = await db.user.findMany() return Response.json(users)}
Server Actions: walidacja i autoryzacja w każdej akcji
Server Actions to operacje POST, które są publicznie dostępne i identyfikowane przez generowane identyfikatory. Należy pamiętać, że choć identyfikator może zmieniać się między buildami i nie stanowi publicznego kontraktu API, nie jest to równoznaczne z kontrolą dostępu. Next.js wzmacnia ochronę przed , usuwając nieużywane akcje, ograniczając metodę do POST oraz porównując Origin z Host lub X-Forwarded-Host. Mimo tych zabezpieczeń, każdą wyeksportowaną akcję należy traktować jako potencjalne wejście do systemu, wymagające odpowiedniego zabezpieczenia.
Code
// actions.ts'use server'import { auth } from '@/lib/auth'import { revalidatePath } from 'next/cache'import { db } from '@/lib/db'import { z } from 'zod'export async function deleteProduct(productId: string) { const session = await auth() if (!session?.user?.id) throw new Error('Unauthorized') const id = z.string().uuid().parse(productId) // Sprawdź uprawnienie do konkretnego zasobu, nie tylko zalogowanie. const result = await db.product.deleteMany({ where: { id, ownerId: session.user.id }, }) if (result.count === 0) throw new Error('Not found') revalidatePath('/products')}
Samo zastosowanie if (session) nie wystarcza, ponieważ poprzednia wersja przykładu pozwalałaby każdemu zalogowanemu użytkownikowi usunąć produkt o znanym UUID (klasyczny IDOR). Uprawnienie sprawdzaj dla konkretnego rekordu, najlepiej atomowo w warunku zapytania, aby uniknąć wyścigu między osobnym odczytem i zapisem.
Route Handlery nie dostają automatycznie takiego samego modelu ochrony CSRF. Jeśli mutują dane na podstawie cookie sesyjnego, weryfikuj origin lub token CSRF i ustaw poprawne atrybuty cookie. Dla API z tokenem w nagłówku zaprojektuj CORS, rate limiting, limity body i jednoznaczne odpowiedzi błędów. W obu mechanizmach waliduj input i autoryzuj operację przy warstwie danych.
Cache, limity i format danych — różnice, które zmieniają decyzję
Route Handlery nie są domyślnie cache'owane. GET możesz jawnie przełączyć na statyczny przez dynamic = 'force-static'. Przy włączonych Cache Components obowiązuje nowszy model z use cache w wywoływanej funkcji. Metody mutujące pozostają dynamiczne. Po mutacji zarówno Route Handler, jak i Server Action mogą wywołać revalidatePath lub revalidateTag. updateTag, zapewniający scenariusz read-your-own-writes, jest dostępny tylko w Server Actions. Rewalidacja nie dzieje się „automatycznie” — integracja Actions pozwala zwrócić świeże UI w tym samym roundtripie, ale to Ty wskazujesz, co unieważnić lub odświeżyć.
Argumenty i wynik Server Function muszą być serializowalne przez React. Nie jest to dobre miejsce na dowolny protokół binarny, kontrolę statusów HTTP czy stabilny format dla innych klientów. Domyślny limit body Server Actions wynosi 1 MB i można go zmienić przez serverActions.bodySizeLimit; zwiększenie limitu nie zastępuje ograniczenia typu oraz rozmiaru pliku ani bezpośredniego uploadu do storage. Route Handler daje pełną kontrolę nad Request i Response, streamingiem, nagłówkami oraz formatem odpowiedzi, ale limity platformy hostingowej nadal obowiązują.
Wzorce architektoniczne dla Route Handlers i Server Actions
Wzorzec 1: Server Actions dla CRUD, Route Handlers dla integracji
// lib/services/product-service.tsimport 'server-only'export async function createProduct(actor: Actor, input: unknown) { if (!actor.permissions.includes('products:create')) { throw new ForbiddenError() } const data = productSchema.parse(input) const product = await db.product.create({ data: { ...data, ownerId: actor.userId }, }) return toProductDto(product)}
Code
// Server Action korzysta z serwisu'use server'import { createProduct } from '@/lib/services/product-service'export async function createProductAction(formData: FormData) { const actor = await requireActor() await createProduct(actor, { name: formData.get('name'), price: formData.get('price'), category: formData.get('category'), }) revalidatePath('/products')}
Code
// Route Handler korzysta z tego samego serwisuimport { createProduct } from '@/lib/services/product-service'export async function POST(request: Request) { const actor = await requireApiActor(request) const input: unknown = await request.json() const product = await createProduct(actor, input) return Response.json(product, { status: 201 })}
Warstwa wspólna powinna egzekwować niezmienne reguły bezpieczeństwa i biznesu, nawet jeśli adapter wcześniej wykonał własną kontrolę. Adapter odpowiada za transport, czyli parsowanie FormData lub JSON, mapowanie błędów na stan formularza albo status HTTP, cookies i nagłówki. Serwis odpowiada za uprawnienia do operacji, reguły domenowe, transakcję i minimalny DTO. Dzięki temu dodanie trzeciego wejścia — np. zadania kolejki — nie omija zasad tylko dlatego, że nie przechodzi przez UI.
Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.
Czy Server Actions mogą zwracać dane jak GET endpoint?
Mogą zwracać dane serializowalne przez React, ale są projektowane do mutacji i transportowo używają POST. Klient obecnie wysyła i oczekuje na Actions pojedynczo, więc nie zastępują równoległego data fetchingu. Do pobierania danych w Server Components używaj bezpośrednich wywołań funkcji (np. db.product.findMany()), nie potrzebujesz ani Route Handlera, ani Server Action.
Czy mogę wywołać Server Action z zewnętrznej aplikacji?
Technicznie tak (to POST request), ale nie jest to wspierane ani zalecane. Server Actions generują tymczasowe identyfikatory, które mogą się zmieniać między buildami. Dla zewnętrznych konsumentów — aplikacji mobilnej, innego serwisu — stwórz Route Handler.
Czy Server Actions działają bez JavaScript?
Formularz renderowany przez Server Component i przekazujący akcję przez action={serverAction} korzysta z progresywnego ulepszania: może zostać wysłany przed załadowaniem JS albo przy wyłączonym JS. W Client Component submit przed hydratacją jest kolejkowany i priorytetyzuje hydratację. Wywołanie akcji z onClick lub useEffect wymaga JavaScriptu.
Czy Server Action jest prywatny, bo napisałem go w pliku use server?
Nie. Server Action jest wywoływany przez POST na automatycznie generowanym endpoincie, więc można go uruchomić z zewnątrz. Zawsze waliduj input i sprawdzaj autoryzację w samej akcji i traktuj ją jak publiczny endpoint API pod względem bezpieczeństwa.
Jak nie duplikować logiki między Route Handlerem a Server Action?
Wydziel logikę biznesową do wspólnego serwisu (np. lib/services/product-service.ts), a Route Handler i Server Action niech tylko go wywołują. Dzięki temu reguły biznesowe, autoryzacja zasobu i operacje na bazie żyją w jednym miejscu. Każdy adapter nadal parsuje swój format wejścia: JSON w Route Handlerze albo FormData w Server Action.
O autorze
Maciej Sala
Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.
W Next.js traktuj Server Actions i Route Handlers jako cienkie adaptery wejścia: pobierają sesję, walidują payload i mapują odpowiedź. Reguły biznesowe trzymaj w Service Layer, zapytania do bazy w Repository, a transakcje spinaj przez Unit of Work. Taki podział nie jest potrzebny w każdym CRUD-zie, ale zaczyna się zwracać, gdy jedna operacja dotyka kilku tabel, kilku wejść albo kilku reguł.
Maciej Sala
Founder StriveLab
Masz działający projekt na Pages Router i chcesz wejść w Server Components, streaming albo Server Actions. Tylko że przepisanie wszystkiego naraz to proszenie się o regresję, więc tego lepiej nie robić. Rozwiązaniem jest Pages i App Router obok siebie w jednym repozytorium i przenoszenie strona po stronie, potem testy i bezpieczne deploye.
Maciej Sala
Founder StriveLab
React 19 pozwala przekazać funkcję bezpośrednio do atrybutu action elementu . Dzięki integracji z Actions, useActionState , useFormStatus i useOptimistic możesz ograniczyć ręczną obsługę onSubmit , pending state oraz optymistycznych aktualizacji , zachowując kontrolę nad błędami i stanem trwałym.