Skip to main content
Practice Problems

Server actions in Next.js

Server Actions are asynchronous functions that run on the server. They let you mutate data (create, update, delete) directly from components without creating separate API endpoints.

How to Define a Server Action

A Server Action is a function marked with the 'use server' directive:

tsx
// actions/problem.ts 'use server' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' export async function solveProblem(problemId: string, userId: string) { await db.user.update({ where: { id: userId }, data: { solvedProblems: { push: problemId } } }) revalidatePath('/problems') }

The directive can be set at the file level (as above) or at the individual function level:

tsx
export default async function Page() { async function handleSubmit(formData: FormData) { 'use server' const name = formData.get('name') await db.user.update({ where: { id: userId }, data: { name } }) } return <form action={handleSubmit}>...</form> }

Using with Forms

Server Actions can be passed directly to a form's action. The form works even without client-side JavaScript (progressive enhancement):

tsx
// app/settings/page.tsx import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' export default async function SettingsPage() { async function updateProfile(formData: FormData) { 'use server' const name = formData.get('name') as string await db.user.update({ where: { id: currentUserId }, data: { name } }) revalidatePath('/settings') } return ( <form action={updateProfile}> <input name="name" placeholder="Name on IT Lead" /> <button type="submit">Save</button> </form> ) }

Using with useTransition

For optimistic updates and loading indication:

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> ) }

Data Validation

Server Actions receive user input, so validation is mandatory:

tsx
'use server' import { z } from 'zod' 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 } }

Revalidation After Mutation

After changing data you need to update the cache:

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') // invalidates Data Cache by tag revalidatePath('/problems') // invalidates Full Route Cache redirect('/problems') // redirects after creation }

Interview tip:

Server Actions are not a replacement for REST APIs. They are designed for data mutations from UI. For public APIs, integrations with other services or webhooks use Route Handlers.

Useful Resources

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?
Practice Problems