Next.js 16 Server Actions: Praktický sprievodca formulármi a mutáciami (2026)

Praktický sprievodca Server Actions v Next.js 16: ako písať formuláre s useActionState, validovať vstupy Zod-om, revalidovať cache a vyhnúť sa typickým bezpečnostným chybám v produkčnom kóde.

Next.js 16 Server Actions: Návod (2026)

Aktualizované: 27. júna 2026

Next.js 16 Server Actions sú asynchrónne funkcie označené direktívou "use server", ktoré bežia výhradne na serveri a umožňujú mutovať dáta priamo z formulárov alebo klientskych komponentov bez potreby vytvárať samostatné API endpointy. V tomto sprievodcovi prejdeme všetkým, čo som sa za posledný rok naučil v produkčných projektoch: od základnej syntaxe, cez validáciu so Zod, revalidáciu vyrovnávacej pamäte, prácu s cookies, až po bezpečnostné nástrahy, ktoré nie sú v dokumentácii dostatočne zvýraznené.

  • Server Actions sú RPC-štýl funkcie, ktoré Next.js automaticky exponuje ako POST endpoint so šifrovaným ID. Nie sú to skryté API trasy.
  • Pre formuláre kombinujte useActionState (React 19) s useFormStatus. Získate stav formulára, pending indikátor aj progresívne vylepšenie bez JavaScriptu.
  • Vždy validujte vstupy na strane servera pomocou Zod alebo Valibot. Klientska validácia je len UX, nie bezpečnosť.
  • Po mutácii vždy zavolajte revalidatePath() alebo revalidateTag(), inak používateľ uvidí starý cache.
  • Closure premenné v Server Actions sú šifrované a posielané klientovi. Používajte experimental_taintObjectReference, aby vám neunikli citlivé dáta.
  • Server Actions nenahrádzajú Route Handlers vo všetkých prípadoch. Pre webhooky, public API alebo streaming naďalej použite route.ts.

Čo sú Server Actions v Next.js 16

Server Actions sú asynchrónne funkcie, ktoré pôvodne pochádzajú z React 19 a v Next.js 16 sú plne stabilné. Označíte ich direktívou "use server", buď na úrovni súboru, alebo priamo nad konkrétnou funkciou, a Next.js ich automaticky vystaví ako šifrovaný POST endpoint. Z pohľadu vývojára píšete obyčajnú serverovú funkciu, no z klientskeho komponentu ju voláte, ako keby išlo o lokálnu funkciu. Pod kapotou si React vymení medzi klientom a serverom správy cez React Server Components wire formát.

V Next.js 16 sa Server Actions integrujú hlbšie s novým use cache mechanizmom a s Partial Prerendering. Najväčší rozdiel oproti starému pages-only modelu je ten, že už nepíšete /pages/api/createPost.ts a manuálne nehandlujete request/response. Namiesto toho exportujete funkciu, ktorá vie dáta zmeniť, prevalidovať a vrátiť výsledok alebo chybu.

Stabilizácia v Next.js 16 priniesla aj striktnejšie predvolené správanie: napríklad inline "use server" v Client Components už hádže chybu kvôli šifrovaniu closure. A to je vlastne dobre. Pri pages routerovskej mentalite som si na to musel chvíľu zvyknúť.

Ako Server Actions vnútorne fungujú

Pri builde Next.js prejde každý súbor s "use server", vygeneruje šifrované ID akcie a zaregistruje ju do manifestu. Keď klient zavolá Server Action, React odošle POST request na rovnakú URL ako aktuálna stránka, v hlavičke Next-Action pošle ID akcie a v tele zoradené argumenty. Server akciu nájde podľa manifestu, dešifruje closure premenné (ak nejaké sú), spustí ju a v odpovedi vráti buď JSON s návratovou hodnotou, alebo aj re-renderovanú časť RSC stromu, ak akcia spôsobí revalidáciu.

Dôležitý detail: ID akcie je deterministicky odvodené z hashu cesty a názvu funkcie šifrovaného serverovým kľúčom. Vďaka tomu klient nikdy nepozná skutočné meno funkcie a Next.js zabráni tomu, aby útočník zavolal akciu, ktorú jeho stránka aktívne nevyužíva. Oficiálna dokumentácia Server Actions popisuje protokol detailnejšie.

Pre dlhoročných Next.js vývojárov je to mentálny posun. Nemyslíte na URL, nemyslíte na HTTP metódy, premýšľate iba o funkciách. Pages router fanúšikom (vrátane mňa) to chvíľu trvá, no v istom bode to klikne.

Základný formulár so Server Action

Najjednoduchší prípad použitia je HTML formulár. Akcia dostane FormData a vy z neho vytiahnete polia. Tento príklad funguje aj bez JavaScriptu, čo je progresívne vylepšenie zadarmo:

// app/posts/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { db } from "@/lib/db";

export async function createPost(formData: FormData) {
  const title = formData.get("title")?.toString().trim();
  const body  = formData.get("body")?.toString().trim();

  if (!title || !body) {
    throw new Error("Názov a obsah sú povinné.");
  }

  const post = await db.post.create({
    data: { title, body, authorId: 1 },
  });

  revalidatePath("/posts");
  redirect(`/posts/${post.id}`);
}
// app/posts/new/page.tsx
import { createPost } from "../actions";

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="body" required />
      <button type="submit">Publikovať</button>
    </form>
  );
}

Všimnite si, že page.tsx je Server Component a action sa odovzdá ako referencia. Next.js ju serializuje a klient vie, kam má odoslať POST. Žiadny fetch(), žiadny onSubmit, žiadne JSON.stringify. Honestly, keď som toto videl prvýkrát, pripadalo mi to ako mágia.

useActionState a useFormStatus v praxi

Vyhadzovať Error z Server Action funguje, ale používateľovi zobrazí len generickú error page. V reálnej aplikácii chcete chyby zobrazovať priamo pri formulári. Na to slúži React 19 hook useActionState v páre s useFormStatus:

"use server";

export type FormState = { error?: string; success?: boolean };

export async function createPost(
  _prev: FormState,
  formData: FormData,
): Promise<FormState> {
  const title = formData.get("title")?.toString().trim() ?? "";
  if (title.length < 3) {
    return { error: "Názov musí mať aspoň 3 znaky." };
  }
  await db.post.create({ data: { title } });
  revalidatePath("/posts");
  return { success: true };
}
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { createPost } from "./actions";

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button disabled={pending}>
      {pending ? "Ukladám…" : "Publikovať"}
    </button>
  );
}

export function PostForm() {
  const [state, action] = useActionState(createPost, {});
  return (
    <form action={action}>
      <input name="title" />
      {state.error && <p role="alert">{state.error}</p>}
      {state.success && <p>Uložené.</p>}
      <SubmitButton />
    </form>
  );
}

Trik je v tom, že useFormStatus musí byť v komponente vnútri formulára. V rovnakom komponente, kde je <form>, nefunguje. React dokumentácia k useActionState tento detail vysvetľuje aj s alternatívnym signature pre permalink fallback.

Validácia vstupu pomocou Zod

Manuálne kontroly typu if (title.length < 3) sú v poriadku pre jeden formulár, ale rýchlo sa neudržia. Pre čokoľvek nad triviálne polia siahnem po Zod alebo Valibot. Schéma žije na serveri, takže môže obsahovať aj asynchrónne kontroly proti databáze:

"use server";

import { z } from "zod";

const PostSchema = z.object({
  title: z.string().min(3).max(120),
  body:  z.string().min(20),
  tags:  z.array(z.string()).max(5).optional(),
});

export async function createPost(_prev: FormState, formData: FormData) {
  const raw = {
    title: formData.get("title"),
    body:  formData.get("body"),
    tags:  formData.getAll("tags"),
  };

  const parsed = PostSchema.safeParse(raw);
  if (!parsed.success) {
    return {
      fieldErrors: parsed.error.flatten().fieldErrors,
    };
  }

  await db.post.create({ data: parsed.data });
  revalidatePath("/posts");
  return { success: true };
}

Revalidácia cache a redirect po mutácii

Toto je najčastejší dôvod, prečo nováčikovia na Server Actions hlásia bug „dáta sa neaktualizujú". Next.js 16 agresívne cachuje fetch volania aj cesty. Ak v Server Action niečo zmeníte, MUSÍTE explicitne povedať, čo zneplatniť:

  • revalidatePath("/posts"): invaliduje konkrétnu cestu vrátane všetkých jej layoutov.
  • revalidatePath("/posts/[id]", "page"): invaliduje dynamickú cestu.
  • revalidateTag("posts"): invaliduje všetky fetch volania a cached funkcie označené týmto tagom.

Druhá kombinácia je redirect() z next/navigation. Volá sa cez throw mechanizmus, takže akýkoľvek kód za redirect() sa nevykoná. Nikdy ho nedávajte do try/catch bloku okolo databázových operácií, lebo chytíte vlastný redirect a používateľ ostane na pôvodnej stránke. Ak používate stratégie ako Partial Prerendering, revalidácia funguje aj pre dynamické časti, ktoré sú v Suspense.

Bezpečnosť: taint, šifrovanie a CSRF

Toto je tá časť, ktorú väčšina tutoriálov skĺzne. Server Actions sú POST endpointy, čo znamená, že na ne platí všetko, čo platí na klasické API: autentifikácia, autorizácia, rate limiting, CSRF.

Autorizácia v každej akcii

Middleware vás chráni pri navigácii, ale nemusí chrániť pri vyvolaní akcie. Pravidlo, ktoré odporúčam dodržiavať bez výnimky: každá Server Action musí znova overiť identitu a oprávnenia, aj keby ste „vedeli", že stránka je len pre prihlásených.

"use server";
import { auth } from "@/lib/auth";

export async function deletePost(id: string) {
  const session = await auth();
  if (!session?.user) throw new Error("Neautorizované.");

  const post = await db.post.findUnique({ where: { id } });
  if (post?.authorId !== session.user.id) {
    throw new Error("Zakázané.");
  }

  await db.post.delete({ where: { id } });
  revalidatePath("/posts");
}

Šifrované closure a taint API

Ak v Server Action „odovzdáte" ako closure premennú niečo citlivé (napríklad interné ID), Next.js to šifruje a posiela klientovi ako handle. Klient to nemôže prečítať, ale ak by ste omylom do closure dali databázový objekt s heslom, šifrovaná verzia by sa stále preniesla a putovala by sieťou. experimental_taintObjectReference z React-u vám umožňuje označiť objekt ako citlivý: pokus o jeho serializáciu hodí build-time chybu.

CSRF

Next.js 16 automaticky kontroluje Origin hlavičku oproti hostiteľovi a odmieta requesty z iných pôvodov. To pokrýva väčšinu CSRF scenárov bez práce. Ak máte multi-doménový setup, povolíte ďalšie pôvody cez experimental.allowedOrigins v next.config.js.

Server Actions vs API Routes: kedy čo

Toto je otázka, ktorá mi chodí na DM aspoň raz týždenne. Krátka odpoveď: Server Actions sú pre internú mutáciu vlastnej aplikácie; Route Handlers (app/api/.../route.ts) sú pre verejné HTTP rozhrania.

KritériumServer ActionsRoute Handlers
Hlavný účelMutácie z UIVerejné API, webhooky
Volanie z mobilnej appkyNie (interný protokol)Áno
Webhook od Stripe / GitHubNieÁno
Streaming responseObmedzenéPlne (ReadableStream)
Progresívne vylepšenie formulárovÁno, natívneManuálne
CSRF ochranaAutomatická (Origin check)Manuálna
Typovo bezpečné volanie z RSCÁnoCez fetch, bez typu
Vlastná URL štruktúraNieÁno

V praxi mám v projektoch oboje vedľa seba. Webhooky, OAuth callbacky a verejné JSON endpointy idú do route.ts. Všetko, čo robí používateľ kliknutím v UI, je Server Action.

Optimistické aktualizácie s useOptimistic

Server Action trvá aspoň jeden round-trip. Pre lajkovanie, komentáre alebo radenie zoznamu chcete UI aktualizovať okamžite a opraviť, ak akcia zlyhá. Na to je React hook useOptimistic:

"use client";
import { useOptimistic, useTransition } from "react";
import { toggleLike } from "./actions";

export function LikeButton({ postId, likes }: { postId: string; likes: number }) {
  const [pending, start] = useTransition();
  const [optimistic, setOptimistic] = useOptimistic(
    likes,
    (state, delta: number) => state + delta,
  );

  return (
    <button
      disabled={pending}
      onClick={() => {
        start(async () => {
          setOptimistic(1);
          await toggleLike(postId);
        });
      }}
    >
      ♥ {optimistic}
    </button>
  );
}

Časté chyby a ako ich opraviť

„Server Actions must be async functions"

Vyskytuje sa, keď zabudnete async. Direktíva "use server" sa môže nachádzať len v asynchrónnych funkciách, aj keď telo nepotrebuje await.

Dáta sa po mutácii neaktualizujú

Chýba revalidatePath/revalidateTag. Druhá možnosť: voláte Server Action z Client Component, ktorá si dáta drží v lokálnom stave. revalidate* obnoví Server Components, ale vašu klientsku state musíte zlikvidovať explicitne (alebo prepnúť na server-driven zobrazenie).

„Body exceeded 1 MB limit"

Predvolený limit pre Server Action body je 1 MB. Pre upload súborov sa neoplatí ho zdvíhať. Namiesto toho použite presigned URL na S3/R2 a Server Action zavolajte až s metadátami. Pokiaľ však naozaj potrebujete väčší limit, nastavte ho v next.config.js cez experimental.serverActions.bodySizeLimit.

Edge runtime a Server Actions

Niektoré akcie potrebujú Node.js API: Buffer, súborový systém, niektoré ORM. Skontrolujte, či vaša cesta beží na Node, nie na Edge. Toto je rovnaká triáda problémov, akú riešime v článku o prechode middleware z Edge na Node.js runtime.

Closure leak

Ak v Server Action použijete vonkajšiu premennú, ktorá pochádza z RSC, šifruje sa do payloadu. Pri obrovských štruktúrach (napríklad celý databázový riadok) sa vám zbytočne nafúkne odpoveď. Pošlite radšej len ID a v akcii si dáta zase načítajte. Pozrite si aj oficiálny security model RSC a Server Actions.

Často kladené otázky

Sú Server Actions v Next.js 16 bezpečné na produkciu?

Áno, Server Actions sú v Next.js 16 stabilné a používajú sa v produkcii vo veľkých aplikáciách. Šifrované ID akcií, automatická CSRF ochrana cez Origin hlavičku a šifrované closure premenné z nich robia bezpečný mechanizmus, pod podmienkou, že v každej akcii znova kontrolujete autentifikáciu a oprávnenia.

Kedy mám použiť Server Action namiesto API route?

Server Action použite vždy, keď mutácia vychádza z vašej vlastnej UI: formuláre, tlačidlá, lajky. API route (route.ts) si nechajte pre verejné JSON endpointy, webhooky tretích strán, OAuth callbacky a prípady, kde potrebujete streaming odpoveď alebo vlastnú URL štruktúru.

Ako validovať vstup zo Server Action?

Validáciu robte výhradne na serveri pomocou Zod alebo Valibot schémy. Klientska validácia slúži len ako UX zlepšenie, nikdy jej neverte. Použite safeParse, výsledok skontrolujte a chyby vráťte cez useActionState, aby ich UI vedelo zobraziť pri konkrétnych poliach.

Prečo sa po Server Action nezobrazia nové dáta?

Najčastejšou príčinou je chýbajúce volanie revalidatePath() alebo revalidateTag() na konci akcie. Next.js 16 cachuje fetch volania aj cesty, takže bez explicitnej revalidácie servíruje predošlú verziu. Druhá možná príčina je, že komponent drží dáta v lokálnom useState, ktorý sa pri serverovej revalidácii neresetuje.

Môžem volať Server Action z mobilnej aplikácie alebo cez curl?

Technicky áno, ale neodporúča sa to. Server Actions používajú interný protokol so šifrovanými ID, ktoré sa môžu pri každom builde zmeniť, a očakávajú špecifické hlavičky. Pre stabilné rozhranie pre mobil alebo externé klienty vytvorte klasický Route Handler v app/api/ a podpíšte ho napríklad cez API kľúč.

Ben Howard
O Autorovi Ben Howard

Full-stack Next.js developer who's been with the framework since pages-only days. Slowly warming up to App Router.