Server Actions

Hard

Server Actions are async functions that execute on the server, triggered from client-side forms and event handlers. Marked with the "use server" directive, they eliminate API route boilerplate for data mutations. They integrate with the form action prop for progressive enhancement, meaning forms work before JavaScript loads and upgrade to rich async experiences after hydration.

Interactive Visualization

Key Points

  • Marked with "use server" directive — can be in a dedicated file or inline in Server Components
  • Passed to form action prop for automatic FormData collection and server execution
  • Enable progressive enhancement: forms work as standard HTML submissions before JavaScript loads
  • Pair with useActionState for managed pending/error state and useOptimistic for instant UI feedback
  • Replace API routes for internal data mutations — no fetch(), no JSON serialization, no route handlers

Code Examples

Basic Server Action

// actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function createTodo(formData: FormData): Promise<void> {
  const text = formData.get('text') as string

  await db.todos.create({
    data: { text, completed: false },
  })

  revalidatePath('/todos')
}

// TodoForm.tsx
import { createTodo } from './actions'

function TodoForm() {
  return (
    <form action={createTodo}>
      <input name="text" placeholder="New todo..." required />
      <button type="submit">Add</button>
    </form>
  )
}

The "use server" directive marks createTodo as a Server Action. When the form submits, the framework serializes FormData, sends it to the server, executes the function, and revalidates the cached page data. No API route, no fetch call, no JSON parsing.

Server Action with useActionState

'use server'

interface TodoState {
  error: string | null
  success: boolean
}

export async function createTodo(
  prevState: TodoState,
  formData: FormData
): Promise<TodoState> {
  const text = formData.get('text') as string

  if (text.length < 2) {
    return { error: 'Todo must be at least 2 characters', success: false }
  }

  try {
    await db.todos.create({ data: { text, completed: false } })
    revalidatePath('/todos')
    return { error: null, success: true }
  } catch {
    return { error: 'Failed to create todo', success: false }
  }
}

// Client component with managed state
'use client'
import { useActionState } from 'react'
import { createTodo } from './actions'

function TodoForm() {
  const [state, action, isPending] = useActionState(
    createTodo,
    { error: null, success: false }
  )

  return (
    <form action={action}>
      <input name="text" required />
      {state.error && <p>{state.error}</p>}
      {state.success && <p>Todo added!</p>}
      <button disabled={isPending}>
        {isPending ? 'Adding...' : 'Add Todo'}
      </button>
    </form>
  )
}

useActionState wraps the Server Action to provide managed state (error/success) and automatic pending tracking. The action signature (prevState, formData) -> nextState creates a clean state machine where each submission produces a new state.

Server Actions vs API Routes

// API ROUTE approach (before Server Actions):
// 1. Create app/api/todos/route.ts
// 2. Parse request body
// 3. Validate data
// 4. Insert into DB
// 5. Return JSON response
// 6. Client: fetch('/api/todos', { method: 'POST', body: ... })
// 7. Client: handle response, update state
// Files: 2 | Boilerplate: ~50 lines

// SERVER ACTION approach:
// 1. Create actions.ts with 'use server'
// 2. Write the action function
// 3. Pass to <form action={}>
// Files: 1 action file | Boilerplate: ~15 lines

// When to use Server Actions:
// - Form submissions and data mutations
// - CRUD operations from UI
// - Any mutation triggered by user interaction

// When to keep API Routes:
// - External API consumers (mobile apps)
// - Webhooks from third-party services
// - Streaming responses (SSE)
// - OAuth callback endpoints

Server Actions are the preferred pattern for internal mutations in Next.js applications. They reduce boilerplate by 60-70% compared to API routes while adding progressive enhancement. API Routes remain necessary for external consumers and specialized response types.

Common Mistakes

  • Using Server Actions for data fetching instead of mutations — Server Components with async/await handle reads, Server Actions handle writes
  • Forgetting to revalidatePath or revalidateTag after a mutation, causing stale cached data
  • Passing non-serializable arguments to Server Actions — all arguments must cross the server boundary as serializable values

Interview Tips

  • Explain progressive enhancement: Server Action forms work as HTML forms before JS loads, then enhance to async
  • Know the difference between Server Components (data fetching at render) and Server Actions (mutations from interactions)
  • Discuss the security model: Server Actions use encrypted action IDs and bound arguments to prevent tampering

Related Concepts