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:
// 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:
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):
// 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 Hack Frontend" />
<button type="submit">Save</button>
</form>
)
}
Using with useTransition
For optimistic updates and loading indication:
'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:
'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:
'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
- nextjs.org/docs — Server Actions
- Forms and Mutations