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