Server Actions in Next.js have been promising since 13. They stabilized properly in Next.js 15 (late 2024) and matured into a default-pattern in 2026. The reduction in boilerplate is real — most form-driven flows that previously needed an API route + fetch + state-management stack now collapse to a single annotated function. Here's the patterns that work.
What changed in 2026
- Stable since Next.js 15.1 — no more breaking changes between minors.
useActionState (formerly useFormState) replaced the older patterns. Cleaner ergonomics.
- Server Actions now support file uploads without third-party libraries.
The minimal Server Action
'use server'
import { z } from 'zod'
const NewPostSchema = z.object({
title: z.string().min(1).max(200),
body: z.string().min(1),
})
export async function createPost(prev: unknown, formData: FormData) {
const parsed = NewPostSchema.safeParse({
title: formData.get('title'),
body: formData.get('body'),
})
if (!parsed.success) return { error: parsed.error.flatten() }
await db.post.create({ data: parsed.data })
return { success: true }
}
Three things to notice: 'use server' directive at the top, Zod validation as the first thing, and a structured return for the client. This is the template — every action follows it.
Auth with Server Actions
The auth-aware pattern: extract user from cookies/session in a helper, fail fast if absent, then proceed. Don't trust the client.
'use server'
export async function deletePost(formData: FormData) {
const user = await getCurrentUser()
if (!user) throw new Error('Unauthorized')
const postId = formData.get('postId')
const post = await db.post.findUnique({ where: { id: postId }})
if (post.authorId !== user.id) throw new Error('Forbidden')
await db.post.delete({ where: { id: postId }})
revalidatePath('/posts')
}
The bug we see most: forgetting to check ownership. The action runs server-side; nothing prevents a malicious user from POSTing arbitrary IDs. Always check.
useActionState + useOptimistic
The clean client pattern. useActionState gives you pending, error, and data states. useOptimistic lets you update the UI immediately while the action runs.
'use client'
const [state, formAction, isPending] = useActionState(createPost, null)
const [optimisticPosts, addOptimistic] = useOptimistic(posts, (current, newPost) => [...current, newPost])
return (
<form action={async (fd) => {
addOptimistic({ title: fd.get('title'), pending: true })
formAction(fd)
}}>
{/* form fields */}
{isPending && <Spinner />}
{state?.error && <ErrorBanner err={state.error} />}
</form>
)
Comparison: Server Actions vs Route Handlers vs tRPC
| Pattern |
Best for |
Skip if |
| Server Actions |
Forms, mutations from your own UI |
You need streaming or webhooks |
| Route Handlers |
Public APIs, webhooks, streaming |
All clients are your own UI |
| tRPC |
Strong end-to-end typing across boundaries |
You don't need typed RPC |
| GraphQL |
Multi-team, multi-client APIs |
One team, one client |
When NOT to use Server Actions
Streaming responses. Server Actions return JSON, not streams. Use Route Handlers with Response.body for token-by-token output.
Public API. Other apps shouldn't call Server Actions. Use Route Handlers.
Webhooks. Stripe, GitHub, etc. need stable URLs and verifiable signatures. Use Route Handlers.
Long-running tasks. Server Actions are HTTP request handlers — they timeout at 30s on Vercel. For background work, queue a job.
Common mistakes to avoid
Skipping Zod (or equivalent) validation. TypeScript stops at the wire. Treat the FormData as untrusted.
Forgetting revalidatePath or revalidateTag. Mutations don't auto-invalidate caches; you have to tell Next which routes need refetching.
Using Server Actions for read-only queries. They work, but it's the wrong tool — use server components or React Query for reads.
Not handling redirect properly. redirect() in a Server Action throws — it's deliberate. Don't wrap in try/catch.
Returning errors as exceptions. Return them as part of the action result. Exceptions hit the error boundary; structured errors give you a clean form-error UI.
FAQ
Are Server Actions slower than API routes?
Same performance — they're route handlers under the hood, with a different invocation pattern.
Can I use Server Actions outside Next.js?
No. They're a Next.js + React feature, dependent on the framework's request handling.
What about CSRF protection?
Built-in. Next.js validates origin and same-site checks automatically.
Should I use them in App Router or Pages Router?
App Router only. Pages Router doesn't support Server Actions directly.
Where to go next
For related guides see Drizzle vs Prisma in 2026, Next.js vs Remix in 2026, and TypeScript strict mode guide for 2026.