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