Server Actions and forms
Safe mutations, useActionState, validation and optimistic UI.
Updated
What a Server Action is
A Server Action is an async function marked with 'use server' that you call from the UI (a form, a button), but that runs on the server. Next automatically creates an endpoint and makes the request for you.
You use them for mutations: creating, editing, deleting, saving. They replace the "API route + fetch + loading state" combo for your own forms.
// features/create-post/api/create-post.ts
'use server'
import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(prev: State, formData: FormData) {
const session = await auth()
if (!session) return { error: 'Unauthorized' }
const title = String(formData.get('title') ?? '').trim()
if (title.length < 3) return { error: 'The title is too short' }
const post = await db.post.create({ data: { title, authorId: session.userId } })
updateTag('posts')
redirect(`/posts/${post.id}`)
}This app saves your progress exactly like this (entities/progress/api/save-progress.ts).
Security — the most important thing
A Server Action is a public POST endpoint. Anyone can call it directly, without your UI, with whatever data they want.
- Check authentication and authorization in every action ("are they logged in?" and "are they allowed on this resource?").
- Validate all input (zod / valibot) —
formDatacan contain anything. - Don't return sensitive data.
Ways to call a Server Action
| Way | Code | When |
|---|---|---|
<form action={fn}> |
receives FormData |
the default for forms; works even without JS |
useActionState(fn, initial) |
you get [state, action, pending] |
showing errors / messages / pending |
<button formAction={fn}> |
several actions in one form | "Save" vs "Delete" |
| a direct call in a handler | await fn(args) |
interactions that aren't forms (a like, autosave) |
A form with useActionState
'use client'
import { useActionState } from 'react'
export function PostForm() {
const [state, action, pending] = useActionState(createPost, { error: null })
return (
<form action={action}>
<input name="title" required />
<p aria-live="polite">{state.error}</p>
<button disabled={pending}>{pending ? 'Saving…' : 'Save'}</button>
</form>
)
}With useActionState, the action receives the previous state as its first argument and the FormData as the second.
Progressive enhancement: a form rendered on the server with a Server Action works even if the JS hasn't loaded yet.
The full flow, with Zod validation and values kept after an error, is in Forms with Server Actions.
Optimistic UI
const [optimisticLikes, addLike] = useOptimistic(likes, (state, n: number) => state + n)
<form action={async () => { addLike(1); await like(postId) }}>The UI changes instantly; if the action fails, React goes back to the real value.
Server Action vs Route Handler
| Server Action | Route Handler |
|---|---|
| forms and buttons in your app | an API consumed by others (mobile, partners) |
| mutations | webhooks (Stripe, a CMS) |
| called sequentially, one at a time | non-HTML responses (RSS, files, public JSON) |
Reading data isn't done through Server Actions — that's what Server Components are for.
Summary
- A Server Action = a
'use server'function called from the UI, run on the server; for mutations. - Every action: auth + authorization + validation.
<form action>+useActionStatefor errors and pending;updateTagafter a mutation.