Suggest an editImprove this articleRefine the answer for “Server actions in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Server Actions** are async server-side functions marked with `'use server'` that handle data mutations directly from React components, without a separate API route. ```tsx 'use server' export async function updateUser(formData: FormData) { await db.user.update({ where: { id }, data: { name: formData.get('name') } }) revalidatePath('/settings') } ``` **Key point:** Server Actions are for UI-driven mutations. For public APIs and webhooks, use Route Handlers.Shown above the full answer for quick recall.Answer (EN)Image**Server Actions** are async functions that run on the server and let you mutate data directly from React components, without a separate API endpoint. ## Theory ### TL;DR - Mark a function with `'use server'` and it runs on the server, never in the browser - Pass it to `<form action={...}>` and the form works even without JavaScript loaded - Call it from a Client Component via `useTransition` to get a loading state - Always validate input - the function is publicly callable like any HTTP endpoint - After every write, call `revalidatePath` or `revalidateTag` to clear stale cached data ### Quick example ```tsx // actions/user.ts 'use server' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' export async function updateUserName(formData: FormData) { const name = formData.get('name') as string await db.user.update({ where: { id: currentUserId }, data: { name } }) revalidatePath('/settings') // tell Next.js to re-render this route } ``` Next.js creates an HTTP POST endpoint for this function automatically. When the form submits, the browser hits that endpoint - no client-side fetch required. ### What makes Server Actions different from Route Handlers Before Server Actions, a form mutation required three files: a Server Component to fetch data, a Client Component to manage state, and a Route Handler to accept the POST. Server Actions collapse all three into one function. The boundary is clear though: they are designed for UI-initiated mutations only. Public APIs, webhooks, and third-party integrations still belong in Route Handlers - a Server Action's URL is an implementation detail that can change between builds. ### How the directive works The `'use server'` directive goes either at the top of a file (marking every export as a Server Action) or inside an individual function: ```tsx // File-level: everything exported here is a Server Action 'use server' export async function createPost(data: PostData) { ... } export async function deletePost(id: string) { ... } ``` ```tsx // Function-level: only this function runs on the server export default async function Page() { async function handleSubmit(formData: FormData) { 'use server' // runs on the server } return <form action={handleSubmit}>...</form> } ``` File-level is the standard pattern in larger codebases. Function-level works fine for one-off actions inside a Server Component. ### Using with forms Pass the action directly to `<form action={...}>`. React intercepts the submission and calls the function. If JavaScript has not loaded yet, the browser submits the form as a regular POST and the action still runs. That is progressive enhancement with zero extra code: ```tsx // app/settings/page.tsx import { updateUserName } from '@/actions/user' export default async function SettingsPage() { return ( <form action={updateUserName}> <input name="name" placeholder="Name on IT Lead" /> <button type="submit">Save</button> </form> ) } ``` ### Using with useTransition Forms cover one scenario. Sometimes you want to trigger a mutation from a button click, not a form submit. Import the action and wrap the call in `startTransition`: ```tsx 'use client' import { useTransition } from 'react' import { solveProblem } from '@/actions/problem' export function SolveButton({ problemId, userId }: { problemId: string userId: string }) { const [isPending, startTransition] = useTransition() return ( <button disabled={isPending} onClick={() => startTransition(() => solveProblem(problemId, userId))} > {isPending ? 'Checking...' : 'Solved'} </button> ) } ``` `isPending` is `true` while the action runs. Use it to disable the button or show a spinner. React 19 added `useActionState`, which gives a cleaner API when you also need to read the value the action returns. ### Data validation A Server Action is callable from anywhere - treat it like an API endpoint. Never trust raw input: ```tsx 'use server' import { z } from 'zod' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' const schema = z.object({ name: z.string().min(2).max(50), email: z.string().email() }) export async function updateUser(formData: FormData) { const result = schema.safeParse({ name: formData.get('name'), email: formData.get('email') }) if (!result.success) { return { error: result.error.flatten().fieldErrors } } await db.user.update({ where: { id: currentUserId }, data: result.data }) revalidatePath('/settings') return { success: true } } ``` Return an object with `error` or `success` rather than throwing. The Client Component reads that return value and can display field-level error messages. ### Cache revalidation after mutation Next.js caches aggressively. After a write, you need to tell it which data is now stale: ```tsx 'use server' import { revalidatePath, revalidateTag } from 'next/cache' import { redirect } from 'next/navigation' export async function createProblem(data: ProblemData) { await db.problem.create({ data }) revalidateTag('problems') // clears Data Cache entries tagged 'problems' revalidatePath('/problems') // clears the Full Route Cache for this path redirect('/problems') // sends the user to the updated page } ``` `revalidateTag` is more targeted: it only clears fetches that were tagged with `{ next: { tags: ['problems'] } }`. `revalidatePath` clears everything cached for that URL. When multiple routes share the same data, `revalidateTag` is the better call. ### Common mistakes **1. Skipping validation** ```tsx // Wrong: trusting raw FormData export async function updateUser(formData: FormData) { await db.user.update({ data: { name: formData.get('name') } }) } // Right: validate before writing const result = schema.safeParse({ name: formData.get('name') }) if (!result.success) return { error: result.error.flatten() } ``` **2. Defining a Server Action inside a Client Component file** A function marked `'use server'` cannot live inside a file that starts with `'use client'`. Define it in a separate file or in a Server Component, then import it. **3. Forgetting to revalidate** The mutation runs but the UI shows old data. In my experience this is the most common bug teams hit when first adopting Server Actions. After every write: `revalidatePath` or `revalidateTag`. If you call `redirect`, Next.js automatically revalidates the destination route. **4. Using Server Actions as a public API** The action URL is generated at build time and is not stable across deploys. External services - mobile apps, third-party webhooks - cannot call it reliably. For anything external, use Route Handlers. **5. Throwing instead of returning errors** ```tsx // Risky: action throws, component hits an Error Boundary await updateUser(formData) // Better: return the error, handle it in the component const result = await updateUser(formData) if (!result.success) setErrors(result.error) ``` ### Real-world usage - Profile settings forms - name, avatar, email updates - Marking items complete - solved problems, read notifications, checked todos - Creating records - new post, new comment, new task - Auth flows - login, logout, signup combined with `redirect` - Optimistic UI - paired with `useOptimistic` (React 19) for instant feedback before the server responds ### Follow-up questions **Q:** What happens if a Server Action throws an unhandled error? **A:** The error propagates to the nearest Error Boundary when the action is called from a Client Component. When called directly from a form, the page re-renders without crashing. Returning error objects instead of throwing gives you predictable, component-level control. **Q:** Can a Server Action redirect the user? **A:** Yes. Call `redirect('/path')` from `next/navigation` inside the action. It works the same as in a Server Component - it throws internally and Next.js intercepts it before it reaches the client. **Q:** What is the difference between a Server Action and a Route Handler? **A:** Server Actions are tied to the React component tree - they receive `FormData` and are called from UI. Route Handlers are standard HTTP endpoints that accept any request format and can be called from mobile apps, third-party services, or cron jobs. You cannot call a Server Action from outside the Next.js app reliably. **Q:** How does Next.js protect Server Actions from CSRF attacks? **A:** Next.js generates a random action ID at build time and verifies it on every POST. This blocks cross-site request forgery by default. Authorization - checking who is allowed to call a specific action - is still your responsibility inside the function. **Q:** Can you call a Server Action from another Server Action? **A:** Yes, but it is a plain async function call with no HTTP involved. Import and call it directly. The `'use server'` directive only creates an HTTP endpoint for calls originating from the client. ## Examples ### Profile update with field-level error display ```tsx // actions/profile.ts 'use server' import { z } from 'zod' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' const profileSchema = z.object({ name: z.string().min(2).max(50), }) export async function updateProfile( prevState: unknown, formData: FormData ) { const result = profileSchema.safeParse({ name: formData.get('name') }) if (!result.success) { return { error: result.error.flatten().fieldErrors } } await db.user.update({ where: { id: currentUserId }, data: result.data, }) revalidatePath('/settings') return { success: true } } ``` ```tsx // app/settings/page.tsx 'use client' import { useActionState } from 'react' import { updateProfile } from '@/actions/profile' export default function SettingsPage() { const [state, action] = useActionState(updateProfile, null) return ( <form action={action}> <input name="name" /> {state?.error?.name && <p>{state.error.name[0]}</p>} <button type="submit">Save</button> </form> ) } ``` `useActionState` wires the action's return value back into component state. The `prevState` parameter receives the previous return value on each call - useful for tracking retries or accumulating validation errors. ### Optimistic update with useOptimistic ```tsx 'use client' import { useOptimistic, useTransition } from 'react' import { toggleSolved } from '@/actions/problem' export function ProblemList({ problems }: { problems: Problem[] }) { const [optimisticProblems, addOptimistic] = useOptimistic( problems, (state, solvedId: string) => state.map(p => (p.id === solvedId ? { ...p, solved: true } : p)) ) const [, startTransition] = useTransition() function handleSolve(problemId: string) { startTransition(async () => { addOptimistic(problemId) // update UI instantly await toggleSolved(problemId) // persist to DB }) } return ( <ul> {optimisticProblems.map(p => ( <li key={p.id}> {p.title} <button onClick={() => handleSolve(p.id)} disabled={p.solved}> {p.solved ? 'Done' : 'Mark solved'} </button> </li> ))} </ul> ) } ``` The list updates before the server responds. If the action fails, React rolls back the optimistic state automatically. ### Auth flow with redirect ```tsx // actions/auth.ts 'use server' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { z } from 'zod' import { verifyCredentials, createSession } from '@/lib/auth' const loginSchema = z.object({ email: z.string().email(), password: z.string().min(8), }) export async function login(formData: FormData) { const result = loginSchema.safeParse({ email: formData.get('email'), password: formData.get('password'), }) if (!result.success) { return { error: 'Invalid input' } } const user = await verifyCredentials(result.data) if (!user) { return { error: 'Wrong email or password' } } const session = await createSession(user.id) cookies().set('session', session.token, { httpOnly: true, secure: true }) redirect('/dashboard') } ``` Validation, credential check, cookie, redirect - all in one function, no Route Handler needed.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.