Next.js Server Actions nemen de verplichting weg om een aparte API-route te schrijven alleen om een formulier af te handelen: je plaatst de mutatielogica in een functie die op de server draait en koppelt het formulier er rechtstreeks aan. Het resultaat is dat je data kunt aanmaken en bijwerken zonder een fetch-aanroep, handmatige JSON-serialisatie of een apart app/api/...-bestand. In dit artikel zet ik Server Actions vanaf nul op met de App Router, voeg ik validatie en foutafhandeling toe, en behandel ik waar je in productie op moet letten.
Wat is een Server Action en hoe werkt het?
Een Server Action is een asynchrone functie waarvan de body begint met de 'use server'-directive. Deze functie draait alleen op de server; de code komt nooit in de client-bundle terecht. Wanneer het formulier wordt verzonden, regelt Next.js de netwerkaanroep tussen browser en server voor je. Jij schrijft alleen de functie, en Next.js maakt er een aanroepbaar eindpunt van.
Er zijn twee manieren om hem te plaatsen: op moduleniveau in een apart bestand met 'use server' bovenaan, of inline gedefinieerd binnen een Server Component. Voor herbruikbare, testbare code geef ik de voorkeur aan de aanpak met een apart bestand.
Schrijf je eerste Server Action
Laten we de action eerst in een eigen bestand zetten. De 'use server' bovenaan het bestand maakt van elke geëxporteerde functie erin een Server Action:
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title')
const body = formData.get('body')
// schrijf hier naar de database (Prisma, Drizzle, enz.)
await db.post.create({ data: { title, body } })
revalidatePath('/posts')
redirect('/posts')
}
De formulierkant is verrassend eenvoudig. Je geeft de functie zelf door aan het action-attribuut, geen URL:
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<textarea name="body" />
<button type="submit">Opslaan</button>
</form>
)
}
Let op: het formulier kan binnen een Server Component leven en werkt zelfs wanneer er geen JavaScript is gedownload. Dat is een natuurlijk gevolg van progressive enhancement.
Valideer de invoer — vertrouw nooit ruwe data
Een Server Action is een publiek bereikbaar eindpunt; hij kan rechtstreeks worden aangeroepen, ongeacht wat je op de client toont. Valideer de binnenkomende FormData dus altijd op de server. Schemagebaseerde validatie met Zod is een nette aanpak:
'use server'
import { z } from 'zod'
const PostSchema = z.object({
title: z.string().min(3, 'Titel moet minstens 3 tekens bevatten'),
body: z.string().min(10),
})
export async function createPost(prevState, formData: FormData) {
const parsed = PostSchema.safeParse({
title: formData.get('title'),
body: formData.get('body'),
})
if (!parsed.success) {
return { errors: parsed.error.flatten().fieldErrors }
}
await db.post.create({ data: parsed.data })
revalidatePath('/posts')
return { success: true }
}
safeParse retourneert een resultaatobject in plaats van een fout te gooien, zodat je validatiefouten terug naar de gebruiker kunt sturen.
Fout- en statusafhandeling: useActionState
Om de fouten die de bovenstaande action retourneert aan het formulier te koppelen, gebruiken we de React-hook useActionState (dit is de naam in React 19 / Next.js 15; in oudere versies was het useFormState). De hook vereist een client-component:
'use client'
import { useActionState } from 'react'
import { createPost } from '@/app/actions'
export function PostForm() {
const [state, formAction, pending] = useActionState(createPost, {})
return (
<form action={formAction}>
<input name="title" />
{state?.errors?.title && <p>{state.errors.title}</p>}
<button disabled={pending}>
{pending ? 'Bezig met opslaan...' : 'Opslaan'}
</button>
</form>
)
}
De derde retourwaarde, pending, is true terwijl de action draait en is perfect om de knop uit te schakelen. Voor fijnmaziger laadindicatoren kun je de hook useFormStatus in een aparte knopcomponent gebruiken.
De cache verversen: revalidatePath en revalidateTag
Na een mutatie moet het scherm de actuele data tonen. Omdat Next.js data agressief cachet, maak je het betreffende pad expliciet ongeldig:
revalidatePath('/posts')— wist de cache van een specifieke route.revalidateTag('posts')— als je jefetch-aanroepen hebt getagd, ververst dit alle data die aan die tag is gekoppeld.redirect('/posts')— stuurt de gebruiker na de mutatie naar een andere pagina; roep dit buiten elktry/catch-blok aan, omdat het intern een speciale fout gooit.
Waar je in productie op moet letten
- Autorisatie is verplicht: een action is een eindpunt. Controleer de sessie/rechten aan het begin van elke action; een knop in de UI verbergen is geen beveiliging.
- Isoleer neveneffecten: verplaats database-, e-mail- en betaalwerk naar pure functies die door de action worden aangeroepen; houd de action een dunne laag.
- Retourwaarden moeten serialiseerbaar zijn: de state die naar de client gaat moet uit eenvoudige objecten/arrays bestaan — geen class-instanties of functies.
- Je hebt mogelijk toch een API nodig: heb je een mobiele app of een externe integratie, dan vervangt een Server Action die niet; dan heb je een echte API nodig.
Veelgestelde vragen
Worden Server Actions alleen met formulieren gebruikt?
Nee. Koppelen aan het action-attribuut is de meest gangbare weg, maar je kunt een Server Action ook aanroepen vanuit een onClick-handler van een knop of binnen startTransition. Formulierkoppeling heeft de voorkeur omdat het zelfs werkt wanneer JavaScript is uitgeschakeld.
Moet ik API-routes helemaal laten vallen?
Nee. Server Actions zijn ideaal voor mutaties die vanuit je eigen UI worden getriggerd. Wanneer je webhooks, externe clients of een publieke REST/JSON-interface nodig hebt, blijven klassieke route handlers (app/api/...) het juiste gereedschap.
Zijn Server Actions veilig?
De infrastructuur is veilig — Next.js beschermt action-identifiers en koppelt verzendingen via versleutelde referenties. Maar de beveiliging van je bedrijfslogica is jouw verantwoordelijkheid: valideer invoer en controleer autorisatie. Zonder die laat je een open eindpunt achter.
Worden formulieren in je project ingewikkeld door netwerkcode? Ik help je graag een schone, gevalideerde en veilige datalaag te bouwen met Next.js Server Actions — neem contact met me op.