Het oude patroon
Drie jaar geleden zag een typische form-submit in een Next.js-app er zo uit:
// app/components/contact-form.tsx (oud)
"use client";
export function ContactForm() {
async function submit(data: FormData) {
const res = await fetch("/api/contact", {
method: "POST",
body: JSON.stringify({
name: data.get("name"),
email: data.get("email"),
}),
headers: { "Content-Type": "application/json" },
});
if (!res.ok) {
// toast, error state, etc.
}
}
return <form action={submit}>...</form>;
}En daarnaast — in een aparte file — een API-route:
// app/api/contact/route.ts
import { z } from "zod";
const schema = z.object({ name: z.string(), email: z.string().email() });
export async function POST(req: Request) {
const body = await req.json();
const data = schema.parse(body);
// ... do work
return Response.json({ ok: true });
}Twee bestanden. Twee zod-schemas die je in sync moet houden. Eén fetch call met de bekende footguns: handmatige headers, JSON-stringify, error-handling die je vaak vergeet. En — niet te onderschatten — een hele blob client JavaScript voor het serialiseren en verzenden van een formulier.
Wat server actions vervangen
In Next 14+ kun je dit hele blok in één bestand zetten:
// app/contact-form.tsx
import { z } from "zod";
const schema = z.object({ name: z.string(), email: z.string().email() });
async function submitContact(formData: FormData) {
"use server";
const data = schema.parse({
name: formData.get("name"),
email: formData.get("email"),
});
// ... do work directly here
return { ok: true };
}
export function ContactForm() {
return <form action={submitContact}>...</form>;
}Geen "use client". Geen fetch. Geen API-route. Eén zod-schema. De form-submit zelf gebeurt via een progressively-enhanced HTTP-post naar dezelfde URL, met of zonder JavaScript.
Wat is er aan de hand?
Server actions worden bundled als een POST-endpoint dat Next zelf genereert, met een protocol-laag erbovenop voor argument-serialisatie. Voor de gebruiker betekent dat:
- De form werkt zonder JavaScript. Vóórdat de page-bundle gehydrateerd is, of in browsers waar JS uit staat, doet het formulier gewoon een POST en redirect terug.
- Geen client-side fetch-orchestratie. Geen optimistic update via TanStack Query, geen handmatige loading-state —
useFormStatusdoet dat in een paar regels. - Het client-side JS-payload daalt. Voor een Lumello-achtige app met 12 formulieren per pagina was de winst ongeveer 40-55% minder client JS. Niet omdat de form-code minder werd, maar omdat het hele fetch/state/validatie-frame uit de client bundle viel.
Wanneer je het níet wil
Server actions zijn geen wondermiddel:
- Bulk-bewerkingen met realtime feedback (denk: een Kanban-board waar je 30 cards tegelijk versleept) — voor die UI's wil je nog steeds een client-side store + optimistic updates. Server actions per drag-end zou je server platleggen.
- Acties die buiten de request-context state nodig hebben (long polling, websockets) — server actions zijn fundamenteel POST-requests. Voor live-state heb je nog steeds een aparte websocket-laag nodig.
- Third-party widgets met eigen state machines (Mapbox draws, Stripe Elements) — die zijn al client-only en je verliest niks door fetch-bij-hen-te-laten.
Conclusie
Server actions zijn niet "de nieuwe REST". Het zijn de juiste default voor 80% van form-submits en mutaties in een modern Next-project. De resterende 20% is waarom je nog steeds een API-laag op de plank houdt.
Voor Lumello en Sealr is alles vanuit het portaal — afspraken maken, deals updaten, NDA-handtekeningen — server-action-driven. Het verschil tegenover een REST-API-aanpak is niet alleen ontwikkelsnelheid; het is een meetbaar kleinere client-bundle die op slechtere netwerken sneller bruikbaar wordt.