App Router daje Ci dwa sposoby na uruchomienie kodu po stronie serwera: i . Wyglądają podobnie — oba działają na serwerze, oba sięgają do bazy i zmiennych środowiskowych. I właśnie to podobieństwo myli, bo każdy z nich rozwiązuje zupełnie inny problem. Użycie jednego tam, gdzie pasuje drugi, mści się później na architekturze.
Najczęstszy błąd to traktowanie ich wymiennie. Route Handlers to klasyczne endpointy HTTP. Server Actions to funkcje serwerowe wołane prosto z komponentów React. Ta różnica nie jest kosmetyczna — pociąga za sobą decyzje o architekturze, bezpieczeństwie i wydajności.
Drobne doprecyzowanie terminologii: w dokumentacji React spotkasz dziś nazwę Server Functions. 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 — 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.
Code
// app/api/products/route.tsimport { NextResponse } from 'next/server'import { db } from '@/lib/db'// GET https://api.example.com/productsexport async function GET(request: Request) { const { searchParams } = new URL(request.url) const category = searchParams.get('category') const products = await db.product.findMany({ where: category ? { category } : undefined, orderBy: { createdAt: 'desc' }, }) return NextResponse.json(products)}// POST https://api.example.com/productsexport async function POST(request: Request) { const body = await request.json() const product = await db.product.create({ data: { name: body.name, price: body.price, category: body.category, }, }) return NextResponse.json(product, { status: 201 })}
Kiedy Route Handlers?
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 — , długie odpowiedzi
Custom auth flow — OAuth callbacks, token refresh
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')! try { const event = stripe.webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET!, ) switch (event.type) { case 'checkout.session.completed': await handleCheckoutComplete(event.data.object) break case 'invoice.payment_failed': await handlePaymentFailed(event.data.object) break } return new Response('OK', { status: 200 }) } catch (error) { return new Response('Webhook error', { status: 400 }) }}
Server Actions — funkcje serwerowe w komponentach
Server Actions to funkcje oznaczone dyrektywą "use server", które wywołujesz bezpośrednio z formularzy HTML lub kodu klienta — bez ręcznego tworzenia endpointów API i fetch requestów.
Code
// app/products/actions.ts'use server'import { revalidatePath } from 'next/cache'import { db } from '@/lib/db'import { z } from 'zod'const productSchema = z.object({ name: z.string().min(2).max(100), price: z.number().positive(), category: z.string(),})export async function createProduct(formData: FormData) { const parsed = productSchema.safeParse({ name: formData.get('name'), price: Number(formData.get('price')), category: formData.get('category'), }) if (!parsed.success) { return { error: 'Nieprawidłowe dane', details: parsed.error.flatten() } } await db.product.create({ data: parsed.data }) revalidatePath('/products') return { success: true }}
// app/api/admin/users/route.tsimport { auth } from '@/lib/auth'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 są wywoływane przez POST request na automatycznie generowanym endpoincie. To znaczy, że można je wywołać z zewnątrz — nie są prywatne tylko dlatego, że napisałeś je w pliku z "use server".
Code
// actions.ts'use server'import { auth } from '@/lib/auth'import { z } from 'zod'export async function deleteProduct(productId: string) { // ZAWSZE sprawdzaj autoryzację — action NIE jest prywatny const session = await auth() if (!session) throw new Error('Unauthorized') // ZAWSZE waliduj input const id = z.string().uuid().parse(productId) await db.product.delete({ where: { id } }) revalidatePath('/products')}
Kluczowa zasada: traktuj Server Actions jak publiczne endpointy pod względem bezpieczeństwa. Waliduj input, sprawdzaj uprawnienia, nie ufaj danym od klienta.
Wzorce architektoniczne
Wzorzec 1: Server Actions dla CRUD, Route Handlers dla integracji
// lib/services/product-service.ts// Logika biznesowa współdzielona między Route Handlers i Server Actionsexport async function getProducts(filters?: ProductFilters) { return db.product.findMany({ where: filters })}export async function createProduct(data: CreateProductInput) { const validated = productSchema.parse(data) return db.product.create({ data: validated })}
Code
// Server Action korzysta z serwisu'use server'import { createProduct } from '@/lib/services/product-service'export async function createProductAction(formData: FormData) { const data = Object.fromEntries(formData) await createProduct(data) revalidatePath('/products')}
Code
// Route Handler korzysta z tego samego serwisuimport { createProduct } from '@/lib/services/product-service'export async function POST(request: Request) { const data = await request.json() const product = await createProduct(data) return Response.json(product, { status: 201 })}
Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.
Czy Server Actions mogą zwracać dane jak GET endpoint?
Nie w tradycyjnym sensie — Server Actions służą do mutacji (POST). 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?
Tak — formularz z action={serverAction} działa jako standardowy HTML POST, także zanim załaduje się JavaScript. To progresywne ulepszanie i fundamentalna przewaga nad podejściem fetch + onClick, które bez JS jest martwe.
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 — 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 walidacja i operacje na bazie żyją w jednym miejscu, niezależnie od tego, którą warstwą wejściową je uruchomisz.
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 rozwija własne projekty side-projecty.
Astro 7 to nowa era szybkości. Dzięki Vite 8 i usprawnieniom kompilacji, skrócisz czas builda nawet o 61%. Sprawdź, kiedy migrować, a kiedy poczekać na stabilizację.
Przeglądanie Agentowe w PageSpeed Insights pokazuje, czy strona jest czytelna dla agentów AI. Sprawdź, co bada Lighthouse i jak przygotować React, Astro i Next.js.