Skip to main content

What are Server Actions?

Server Actions are asynchronous functions that run on the server. They enable you to handle form submissions and data mutations without building a separate API.
Server Actions are marked with the 'use server' directive and can be called from both Server Components and Client Components.

Key Benefits

No API Routes

Handle mutations without creating API endpoints

Progressive Enhancement

Forms work without JavaScript enabled

Type Safety

Full TypeScript support end-to-end

Revalidation

Automatically revalidate cached data

Creating a Server Action

There are two ways to define Server Actions:

Inline Server Action

Define the action directly in a Server Component:
app/meals/share/page.js
import ImagePicker from '@/components/meals/image-picker';
import classes from './page.module.css';

export default function ShareMealPage() {
  async function shareMeal(formData) {
    'use server';

    const meal = {
      title: formData.get('title'),
      summary: formData.get('summary'),
      instructions: formData.get('instructions'),
      image: formData.get('image'),
      creator: formData.get('name'),
      creator_email: formData.get('email')
    }

    console.log(meal);
  }

  return (
    <>
      <header className={classes.header}>
        <h1>
          Share your <span className={classes.highlight}>favorite meal</span>
        </h1>
        <p>Or any other meal you feel needs sharing!</p>
      </header>
      <main className={classes.main}>
        <form className={classes.form} action={shareMeal}>
          <div className={classes.row}>
            <p>
              <label htmlFor="name">Your name</label>
              <input type="text" id="name" name="name" required />
            </p>
            <p>
              <label htmlFor="email">Your email</label>
              <input type="email" id="email" name="email" required />
            </p>
          </div>
          <p>
            <label htmlFor="title">Title</label>
            <input type="text" id="title" name="title" required />
          </p>
          <p>
            <label htmlFor="summary">Short Summary</label>
            <input type="text" id="summary" name="summary" required />
          </p>
          <p>
            <label htmlFor="instructions">Instructions</label>
            <textarea
              id="instructions"
              name="instructions"
              rows="10"
              required
            ></textarea>
          </p>
          <ImagePicker label="Your image" name="image" />
          <p className={classes.actions}>
            <button type="submit">Share Meal</button>
          </p>
        </form>
      </main>
    </>
  );
}
  1. User fills out the form
  2. Form submits to the shareMeal action
  3. NextJS automatically serializes form data
  4. Function executes on the server
  5. Console.log appears in your terminal (not browser!)

Separate Server Actions File

For better organization and reusability, create a separate actions file:
lib/actions.js
'use server';

export async function shareMeal(formData) {
  const meal = {
    title: formData.get('title'),
    summary: formData.get('summary'),
    instructions: formData.get('instructions'),
    image: formData.get('image'),
    creator: formData.get('name'),
    creator_email: formData.get('email'),
  };

  console.log(meal);
}
When using 'use server' at the file level, all exported functions in that file become Server Actions.
Then import and use it:
app/meals/share/page.js
import { shareMeal } from '@/lib/actions';

export default function ShareMealPage() {
  return (
    <form action={shareMeal}>
      {/* form fields */}
    </form>
  );
}

Real-World Example: User Management

Here’s a complete example from the course:
actions/users.js
'use server';

import fs from 'node:fs';

export async function saveUserAction(formData) {
  console.log('Executed');
  const data = fs.readFileSync('dummy-db.json', 'utf-8');
  const instructors = JSON.parse(data);
  const newInstructor = {
    id: new Date().getTime().toString(),
    name: formData.get('name'),
    title: formData.get('title'),
  };

  instructors.push(newInstructor);
  fs.writeFileSync('dummy-db.json', JSON.stringify(instructors));
}

Using in a Client Component

components/ServerActionsDemo.js
'use client';

import { saveUserAction } from '@/actions/users';

export default function ServerActionsDemo() {
  return (
    <div className="rsc">
      <h2>Server Actions</h2>
      <p>
        A "Form Action" converted to a "Server Action" via{' '}
        <strong>"use server"</strong>.
      </p>
      <p>Can be defined in a server component or a separate file.</p>
      <p>Can be called from inside server component or client component.</p>
      <form action={saveUserAction}>
        <p>
          <label htmlFor="name">User name</label>
          <input type="text" id="name" name="name" required />
        </p>
        <p>
          <label htmlFor="title">Title</label>
          <input type="text" id="title" name="title" required />
        </p>
        <p>
          <button>Save User</button>
        </p>
      </form>
    </div>
  );
}
Server Actions can be called from both Server Components and Client Components. The function always executes on the server.

Form Validation

Add server-side validation to protect your data:
lib/actions.js
'use server';

import { redirect } from 'next/navigation';
import { saveMeal } from './meals';

function isInvalidText(text) {
  return !text || text.trim() === '';
}

export async function shareMeal(formData) {
  const meal = {
    title: formData.get('title'),
    summary: formData.get('summary'),
    instructions: formData.get('instructions'),
    image: formData.get('image'),
    creator: formData.get('name'),
    creator_email: formData.get('email'),
  };

  if (
    isInvalidText(meal.title) ||
    isInvalidText(meal.summary) ||
    isInvalidText(meal.instructions) ||
    isInvalidText(meal.creator) ||
    isInvalidText(meal.creator_email) ||
    !meal.creator_email.includes('@') ||
    !meal.image ||
    meal.image.size === 0
  ) {
    throw new Error('Invalid input');
  }

  await saveMeal(meal);
  redirect('/meals');
}
Always validate on the server! Client-side validation can be bypassed. Server-side validation is your security layer.

Handling Form State with useFormState

Use the useFormState hook to return validation errors to the user:
lib/actions.js
'use server';

import { redirect } from 'next/navigation';
import { saveMeal } from './meals';

function isInvalidText(text) {
  return !text || text.trim() === '';
}

export async function shareMeal(prevState, formData) {
  const meal = {
    title: formData.get('title'),
    summary: formData.get('summary'),
    instructions: formData.get('instructions'),
    image: formData.get('image'),
    creator: formData.get('name'),
    creator_email: formData.get('email'),
  };

  if (
    isInvalidText(meal.title) ||
    isInvalidText(meal.summary) ||
    isInvalidText(meal.instructions) ||
    isInvalidText(meal.creator) ||
    isInvalidText(meal.creator_email) ||
    !meal.creator_email.includes('@') ||
    !meal.image ||
    meal.image.size === 0
  ) {
    return {
      message: 'Invalid input.',
    };
  }

  await saveMeal(meal);
  redirect('/meals');
}
  1. Action now receives prevState as first parameter
  2. formData becomes second parameter
  3. Return an object with error messages instead of throwing
  4. Component can display the returned state

Using in Component

components/ShareMealForm.js
'use client';

import { useFormState } from 'react-dom';
import { shareMeal } from '@/lib/actions';

export default function ShareMealForm() {
  const [state, formAction] = useFormState(shareMeal, { message: null });

  return (
    <form action={formAction}>
      {state.message && <p className="error">{state.message}</p>}
      
      <p>
        <label htmlFor="title">Title</label>
        <input type="text" id="title" name="title" required />
      </p>
      
      {/* other form fields */}
      
      <button type="submit">Share Meal</button>
    </form>
  );
}

Form Submission Status with useFormStatus

Show loading state during form submission:
components/SubmitButton.js
'use client';

import { useFormStatus } from 'react-dom';

export default function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Share Meal'}
    </button>
  );
}
useFormStatus must be used in a component that is a child of the form, not in the component that renders the form itself.

Cache Revalidation

Invalidate cached data after mutations:
lib/actions.js
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { saveMeal } from './meals';

export async function shareMeal(prevState, formData) {
  // ... validation and save logic
  
  await saveMeal(meal);
  
  // Revalidate the meals page cache
  revalidatePath('/meals');
  
  redirect('/meals');
}
1

Submit Form

User submits the form with new meal data
2

Server Action Runs

shareMeal validates and saves the meal
3

Cache Invalidation

revalidatePath('/meals') marks the meals cache as stale
4

Redirect

User redirected to meals page
5

Fresh Data

Page re-renders with updated data

Revalidation Options

revalidatePath('/meals');

Progressive Enhancement

Server Actions work even if JavaScript is disabled:
<form action={shareMeal}>
  <input type="text" name="title" required />
  <button type="submit">Submit</button>
</form>
This form will work with or without JavaScript! NextJS handles the submission server-side automatically.

Error Handling

Handle errors gracefully in Server Actions:
lib/actions.js
'use server';

export async function shareMeal(prevState, formData) {
  try {
    const meal = {
      title: formData.get('title'),
      // ... other fields
    };

    // Validation
    if (isInvalidText(meal.title)) {
      return {
        message: 'Invalid input.',
      };
    }

    await saveMeal(meal);
    revalidatePath('/meals');
    redirect('/meals');
  } catch (error) {
    return {
      message: 'Failed to save meal. Please try again.',
    };
  }
}

Best Practices

Never trust client data. Validate all inputs in your Server Actions.
Keep Server Actions in dedicated files (e.g., lib/actions.js) for better organization and reusability.
Use useFormState to return specific error messages that help users fix issues.
Always call revalidatePath() or revalidateTag() after data changes to keep cache fresh.
Use useFormStatus to provide feedback during form submission.

Server Actions vs API Routes

FeatureServer ActionsAPI Routes
SetupMinimal (just 'use server')Create separate files
Type SafetyFull end-to-endManual type definitions
Form IntegrationNative HTML formsRequires fetch/axios
Progressive EnhancementYesNo
RevalidationBuilt-inManual
Use CaseForm submissions, mutationsPublic APIs, webhooks
Use Server Actions for internal data mutations. Use API Routes when you need a public API endpoint or webhook.

Complete Example: Full CRUD Flow

1

Create Action File

lib/actions.js
'use server';

export async function createMeal(formData) { /* ... */ }
export async function updateMeal(id, formData) { /* ... */ }
export async function deleteMeal(id) { /* ... */ }
2

Build Form Component

components/MealForm.js
'use client';
import { useFormState } from 'react-dom';
import { createMeal } from '@/lib/actions';
// Component code...
3

Add Validation

Validate inputs server-side and return errors
4

Revalidate Cache

Invalidate affected pages after mutations
5

Redirect

Navigate user to success page

Next Steps

Database Integration

Connect Server Actions to PostgreSQL, MongoDB, or other databases

File Uploads

Handle file uploads in Server Actions

Authentication

Protect Server Actions with authentication

Optimistic Updates

Update UI immediately before server confirmation

Build docs developers (and LLMs) love