Overview
A Server Action is a React Server Function invoked through an action mechanism: <form action>, <button formAction>, or a call inside a transition. The client holds only a reference; the body runs on the server. Actions are the default for mutations in Next.js 16 and integrate with cache invalidation so one round trip returns both the result and the re-rendered page. Form mechanics (useActionState, useOptimistic, FormData) are in react-forms.
Mark actions with "use server"
Put "use server" at the top of an async function body, or at the top of a module to make every export an action; a module-level file may export only async functions. A Client Component can import an action and the implementation never ships to the browser.
// app/notes/actions.ts
"use server";
import { updateTag } from "next/cache";
export async function createNote(formData: FormData) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
await db.note.create({ data: { title: String(formData.get("title")), ownerId: session.user.id } });
updateTag("notes");
}Treat every action as a public POST endpoint
An action is reachable by anyone who can send the same POST, whether or not your UI renders the form. Next.js adds protections: an Origin versus Host check against CSRF, a 1 MB body limit, encrypted action IDs, removal of unused actions from client bundles, and encryption of closure variables. These are not a substitute for application checks. In every action:
- Authenticate and authorize. Rendering a form only on an authenticated page is not a security boundary.
- Validate all input with a schema;
FormDataand headers are untrusted. - Accept an ID plus the user’s change, then re-read the row with the session’s ownership constraint. A well-formed object from the client can still point at someone else’s data.
- Return only what the UI renders, not raw database records.
export async function deletePost(postId: string) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
await db.post.delete({ where: { id: postId, ownerId: session.user.id } });
updateTag("posts");
}Do not rely on the proxy for this: a matcher that skips a path also skips actions on it (see nextjs-middleware).
Configure origins, body size, and the encryption key
// next.config.js
module.exports = {
experimental: {
serverActions: { allowedOrigins: ["my-proxy.com", "*.my-proxy.com"], bodySizeLimit: "2mb" },
},
};Add allowedOrigins when a proxy or CDN changes the host and the action is rejected. On multiple instances or self-hosted deployments, set a stable NEXT_SERVER_ACTIONS_ENCRYPTION_KEY so every instance can decrypt closure variables and action references.
Choose the cache update by the behavior you need
updateTag(tag): expires immediately; the next read waits for fresh data. Use for read-your-own-writes. Server Actions only.revalidateTag(tag, "max"): stale-while-revalidate. The action’s response does not wait for fresh data.revalidatePath(path): one route, when tagging is overkill.refresh(): refetches uncached data on the current route without touching the cache.
updateTag, revalidatePath, refresh, and setting cookies re-render the current route inside the action’s own response. redirect() throws a control-flow exception, so call revalidation before it and never wrap it in try/catch. The model is in nextjs-caching.
Expect sequential dispatch from the client
Next.js runs actions from one client one at a time, so a second call waits for the first. Do not use Promise.all to parallelize actions. Do parallel work inside one action, fetch reads in a Server Component, or use a route handler for non-mutations.
Handle deployment skew
Action IDs change between deployments (Next.js rotates them at most every 14 days even without source changes), so a stale tab can call an ID that no longer exists and fail with “Failed to find Server Action”. Prefer rolling deployments, keep the encryption key stable, and show a retry or refresh path instead of a hard failure.
Pick actions for mutations and handlers for endpoints
Use an action for any mutation a React component triggers: it gives type safety, cache integration, and progressive enhancement. Use a route handler for webhooks, public APIs, and non-React clients.