Next.js Server Actions su asinkrone funkcije s direktivom "use server" koje se izvršavaju isključivo na serveru, a možeš ih pozvati iz Server ili Client komponenata kao obične JavaScript funkcije. Next.js u pozadini koristi HTTP POST sa zaštitom protiv CSRF-a. Od stabilizacije u Next.js 14 i punog dolaska React 19 hookova (useActionState, useFormStatus) u Next.js 15, postale su standardni način za rad s formama, mutacijama baze podataka i revalidacijom keša, bez ručnog pisanja API endpointova.
Server Actions zamjenjuju velik dio route.ts handlera za POST/PUT/DELETE mutacije (kraći kod i bolja DX), ali API rute i dalje imaju mjesta za webhookove i javne endpointove.
Od React 19 i Next.js 15 koristi useActionState umjesto deprecated useFormState. Povratni tip se promijenio, a pending status sada dolazi direktno iz hooka.
revalidatePath i revalidateTag su jedini ispravni način za invalidaciju keša nakon mutacije. Manualno router.refresh() u većini slučajeva nije potreban.
Server Actions su javno dostupni endpointovi, pa uvijek validiraj input (Zod ili Valibot) i provjeri autorizaciju unutar same funkcije, ne samo na klijentu.
Progressive enhancement radi out-of-the-box: forme s action={serverAction} šalju zahtjev i bez JavaScripta na klijentu.
Ne pozivaj Server Action iz useEffect za dohvaćanje podataka. To je krivi alat; koristi async Server Component ili Route Handler.
Što su Next.js Server Actions i kako rade?
Server Action je obična async funkcija označena direktivom "use server", bilo na vrhu datoteke (cijela datoteka postaje "server-only") ili unutar same funkcije. Next.js prilikom builda prepoznaje takve funkcije, generira jedinstveni endpoint za svaku od njih i automatski ih veže na progresivno poboljšane forme ili na izravne pozive iz Client komponenata.
Pod haubom je riječ o običnom POST zahtjevu prema istoj ruti. Next.js šalje serijalizirane argumente, izvršava funkciju u Node ili Edge runtimeu (ovisno o segment konfiguraciji), i vraća rezultat zajedno s eventualnim revalidiranim podacima. Klijent ne zna koji se kod izvršava na serveru jer bundle ne sadrži nikakav server-side kod.
Glavna razlika u odnosu na pages-router doba je da ne moraš pisati ručni fetch. Forma s action={kreirajPost} radi i bez JavaScripta (progressive enhancement), a typesafe pozivi iz Client komponenata izgledaju kao obični function call. Iskreno, u mojoj praksi ovo je najveći mind-shift za ekipe koje dolaze iz Express + REST svijeta. JavaScript funkcija se "magično" izvršava preko mreže, i prvi put kad to vidiš, malo te zbuni.
Krenimo s minimalnim, ali realnim primjerom: forma koja sprema kontakt poruku u bazu podataka. Koristim Prisma u primjerima, ali isti obrazac vrijedi za Drizzle, Kysely ili sirovi SQL.
Prvo, definiraj akciju u zasebnoj datoteci app/actions/contact.ts:
// app/actions/contact.ts
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function posaljiPoruku(formData: FormData) {
const ime = formData.get("ime")?.toString().trim() ?? "";
const poruka = formData.get("poruka")?.toString().trim() ?? "";
if (!ime || !poruka) {
throw new Error("Ime i poruka su obavezni");
}
await prisma.kontaktPoruka.create({
data: { ime, poruka, kreirano: new Date() },
});
revalidatePath("/kontakt");
redirect("/kontakt/uspjeh");
}
Datoteka počinje s "use server", što znači da je svaka exportana funkcija unutar nje Server Action. Forma u Server komponenti može je koristiti direktno:
Ovo je sve. Forma će raditi bez ijedne linije client-side JavaScripta, što Next.js naziva "HTML-first" pristupom. Ako korisnik ima slabu mrežu ili je JS isključen, submit i dalje prolazi standardnim multipart/form-data zahtjevom.
useActionState u React 19: pending, errors i optimistic updates
Kada akcija mora vratiti rezultat klijentu (greške, success poruke, novi podaci), koristi React 19 hook useActionState. Ovo je zamjena za useFormState iz Reacta 18, koji je još uvijek dostupan ali deprecated. React dokumentacija za useActionState detaljno opisuje promjenu API-ja.
Bitno, signature Server Actiona se mijenja: prvi argument postaje prevState, a tek drugi formData.
// app/actions/contact.ts
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
type StanjeForme = {
greske?: Record<string, string>;
uspjeh?: boolean;
};
export async function posaljiPoruku(
_prevState: StanjeForme,
formData: FormData,
): Promise<StanjeForme> {
const ime = formData.get("ime")?.toString().trim() ?? "";
const poruka = formData.get("poruka")?.toString().trim() ?? "";
const greske: Record<string, string> = {};
if (ime.length < 2) greske.ime = "Ime mora imati barem 2 znaka";
if (poruka.length < 10) greske.poruka = "Poruka je prekratka";
if (Object.keys(greske).length > 0) return { greske };
await prisma.kontaktPoruka.create({ data: { ime, poruka, kreirano: new Date() } });
revalidatePath("/kontakt");
return { uspjeh: true };
}
useFormStatus mora biti pozvan unutar komponente renderirane unutar<form>, ne u istoj komponenti gdje je forma definirana. To je čest jamb. Funkcija će ti uvijek vraćati pending: false ako ovo prekršiš. (Stao sam na ovu grabu u prvom projektu s React 19, pa znam koliko frustrira.)
Validacija inputa s Zod-om i tipizirane greške
Ručna validacija (kao gore) brzo postane neodrživa. U produkciji koristim Zod za schema-driven validaciju koja istovremeno daje TypeScript tipove i runtime greške. Evo refaktoriranog primjera:
// app/actions/contact.ts
"use server";
import { z } from "zod";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
const SchemaPoruke = z.object({
ime: z.string().min(2, "Ime mora imati barem 2 znaka"),
email: z.string().email("Neispravan email"),
poruka: z.string().min(10, "Poruka mora imati barem 10 znakova").max(2000),
});
type StanjeForme = {
greske?: Partial<Record<keyof z.infer<typeof SchemaPoruke>, string>>;
uspjeh?: boolean;
};
export async function posaljiPoruku(
_prev: StanjeForme,
formData: FormData,
): Promise<StanjeForme> {
const rezultat = SchemaPoruke.safeParse({
ime: formData.get("ime"),
email: formData.get("email"),
poruka: formData.get("poruka"),
});
if (!rezultat.success) {
const greske: StanjeForme["greske"] = {};
for (const issue of rezultat.error.issues) {
const key = issue.path[0] as keyof z.infer<typeof SchemaPoruke>;
greske[key] = issue.message;
}
return { greske };
}
await prisma.kontaktPoruka.create({ data: rezultat.data });
revalidatePath("/kontakt");
return { uspjeh: true };
}
safeParse nikad ne baci exception. Vraća discriminated union { success: true, data } ili { success: false, error }. Kombinacija sa useActionState daje ti potpuno typesafe rezultat na klijentu, bez ijednog any.
Revalidacija keša: revalidatePath vs revalidateTag
Nakon mutacije, podaci u Next.js Full Route Cache su zastarjeli. Imaš dva načina za invalidaciju, i biranje krivog jednog je razlog #1 zašto "moja stranica pokazuje stare podatke nakon submita". Za dublji uvid u to kako se podaci dohvaćaju i keširaju, pročitaj naš vodič za React Server Components i dohvaćanje podataka.
Karakteristika
revalidatePath
revalidateTag
Što invalidira
Konkretnu rutu (ili sve rute koje matchaju layout)
Sve fetch pozive označene tagom
Granularnost
Po URL-u
Po entitetu (npr. svi pozivi za "post-42")
Najbolji slučaj
"Nakon spremanja, osvježi /blog stranicu"
"Nakon edita posta, osvježi sve gdje se taj post pojavljuje"
Sintaksa fetcha
Nije potrebna
fetch(url, { next: { tags: ["post-42"] } })
Tipičan footgun
Zaboraviti layout kao drugi argument
Zaboraviti dodati tag na sve relevantne fetcheve
Primjer s revalidateTag za blog post koji se pojavljuje na home, kategoriji i pojedinačnoj stranici:
// lib/posts.ts
export async function dohvatiPost(id: string) {
const res = await fetch(`https://api.primjer.com/posts/${id}`, {
next: { tags: [`post-${id}`] },
});
return res.json();
}
// app/actions/posts.ts
"use server";
import { revalidateTag } from "next/cache";
export async function azurirajPost(id: string, formData: FormData) {
await db.update(/* … */);
revalidateTag(`post-${id}`); // pogađa SVE rute koje koriste ovaj post
}
U Next.js 15 s eksperimentalnim Cache Components i "use cache" direktivama, semantika se dodatno mijenja. Ali za stabilan produkcijski kod, revalidatePath i revalidateTag su i dalje pravi izbor.
Server Actions vs API Routes: kada koristiti što?
Ovo je pitanje koje dobivam najviše. Kratki odgovor: Server Actions za interne mutacije, Route Handlers za sve što treba biti javno dostupno preko HTTP-a.
Slučaj korištenja
Server Actions
Route Handlers (route.ts)
Forme i mutacije unutar app-a
Da
Pretjerano
Webhookovi (Stripe, GitHub)
Ne (potreban stabilan URL)
Da
Javni REST/JSON API
Ne
Da
OAuth callbackovi
Ne
Da
Streaming odgovora
Ograničeno
Da (ReadableStream)
Progressive enhancement
Da, ugrađeno
Ne
Mobilna apk konzumacija
Ne
Da
U praksi koristim hibridni pristup: app/api/webhooks/stripe/route.ts za webhookove, a sve interne mutacije (admin panel, korisničke postavke, CRUD) idu kroz Server Actions. To je 80% manje boilerplate koda u tipičnom SaaS dashboardu.
Jesu li Server Actions sigurne? Autorizacija i CSRF
Server Actions jesu sigurne uz pravilno korištenje, ali imaju nekoliko zamki koje moraš svjesno izbjeći. Next.js automatski generira CSRF zaštitu provjerom Origin headera u odnosu na Host, što ti je besplatno.
Ono što nije besplatno je autorizacija. Svaka Server Action je javno dostupan endpoint, čak i ako je definirana unutar admin layouta. Authorization mora ići unutar same akcije:
Middleware ne pokriva Server Actions na način na koji bi očekivao, pa provjere autorizacije moraju živjeti unutar same akcije. Za dublji uvid u to što middleware može a što ne pokriva pri rutiranju, naš Next.js Middleware vodič ulazi u detalje.
Dodatno, u Next.js 15 imaš opciju experimental.serverActions.allowedOrigins u next.config.js za eksplicitno whitelistanje domena, što je preporučeno za produkciju iza reverse proxyja. Next.js 15 release notes pokrivaju i ostale sigurnosne promjene. Za dinamičke SEO obrasce na stranicama koje proizvode mutacije, vrijedi pogledati i Next.js Metadata API vodič.
Česti problemi i kako ih izbjeći
Evo problema koje vidim najčešće u code reviewima:
1. "Server Actions must be async functions"
Direktiva "use server" radi samo s async funkcijama. Sync funkcija će failati u buildu. Ako koristiš strijemu hookova poput onClick, wrappuj poziv u async handler.
2. FormData je prazan unutar akcije
Svaki <input> mora imati name atribut. Bez njega ne završava u FormData. Ovo je classic gotcha za ekipu koja dolazi iz controlled-input svijeta.
3. redirect() baca exception
Internally redirect() radi kroz throw, što znači da ga ne smiješ stavljati u try/catch blok. Bit će uhvaćen i preusmjeravanje neće raditi. Stavi ga poslije try/catch.
4. Stranica i dalje pokazuje stare podatke
Skoro uvijek zaboravljen revalidatePath ili revalidateTag. Drugi razlog je cache: "force-cache" na fetchu bez tagova, tada revalidacija nema kako pogoditi taj fetch.
5. Action se ne builda jer "uses environment variables"
Ako importaš modul koji koristi process.env.DATABASE_URL u datoteku koja se evaluira i na klijentu (npr. shared util), Next.js će vrisnuti. Rješenje: drži DB klijent u "use server" datoteci ili server-only moduleu (import "server-only" na vrhu).
Često postavljana pitanja
Mogu li koristiti Server Actions u Client komponentama?
Da. Server Action možeš importati u Client komponentu i pozvati je kao bilo koju async funkciju, ili je proslijediti kao action prop u <form>. Next.js automatski generira RPC endpoint i tipovi su sačuvani.
Trebam li još uvijek API rute u Next.js 15?
Da, ali manje. Route Handlers su i dalje obavezni za webhookove, OAuth callbackove, javne JSON API-je i streaming odgovore. Za sve interne forme i mutacije, Server Actions su bolji izbor zbog manje boilerplate koda i ugrađene CSRF zaštite.
Kako uploadati datoteku kroz Server Action?
Datoteku dohvati iz formData.get("file") kao File objekt, pa pozovi file.arrayBuffer() ili file.stream(). Provjeri file.size protiv limita i file.type protiv whitelist liste MIME tipova. Imaš podrazumijevani limit od 1MB za body, koji možeš povećati u next.config.js.
Rade li Server Actions s React Server Components keširanjem?
Da. Server Actions su zapravo i napravljene za rad s RSC keširanjem. Nakon mutacije pozoveš revalidatePath ili revalidateTag da invalidiraš keširane fragmente, i sljedeći render dohvaća svježe podatke. Tijesna integracija je glavni razlog zašto Server Actions postoje.
Kako testirati Server Actions?
Za jedinične testove, pozovi akciju izravno kao običnu async funkciju s mockanim FormData objektom (nije potrebna mreža). Za end-to-end testove koristi Playwright koji submita formu i provjerava DOM. Integracijski testovi s Vitest-om plus testing-library/react za Client komponente pokrivaju srednju razinu.
Praktičan vodič kroz Next.js 15 Metadata API u App Routeru: generateMetadata, dinamičke Open Graph slike, JSON-LD strukturirani podaci i kanonski URL-ovi — s primjerima koje možete odmah iskoristiti.
Naučite kako koristiti paralelne rute i intercepting routes u Next.js App Routeru za izgradnju modala, dashboarda, tabova i uvjetnog renderiranja — s praktičnim primjerima koda i najboljim praksama.
Praktičan vodič za React Server Components u Next.js App Routeru. Od kompozicijskih obrazaca i strategija renderiranja do keširanja, streaminga i paralelnog dohvaćanja podataka — sve što vam treba za performantne aplikacije.