Next.js Server Actions

Server Actions are functions that run on the server and can be called directly from your components — including client components. They replace the need to create an API route just to handle a form submission or a button click that modifies data. You write a function, mark it as a server action, and call it from anywhere in your app.

The Problem Server Actions Solve

Before Server Actions, updating data from a form required three things: a form component, an API route to receive the submission, and a fetch call to connect them. Server Actions collapse all three into one function.

OLD WAY (API Route approach):
1. User fills form
2. JavaScript sends POST fetch to /api/create-post
3. API route handler receives data
4. API route saves to database
5. Returns JSON response
6. Frontend handles response

NEW WAY (Server Action):
1. User fills form
2. Form calls server action function directly
3. Server action saves to database
4. Done — no API route, no fetch needed

Creating a Server Action

Add 'use server' at the top of a function or a file to mark it as a server action. Server actions always run on the server, even when called from a client component.

// app/actions.js
'use server';

import { db } from '@/lib/db';

export async function createPost(formData) {
  const title = formData.get('title');
  const content = formData.get('content');

  await db.posts.create({
    data: { title, content }
  });
}

Using a Server Action with a Form

Pass the server action to the HTML form's action attribute. When the form submits, Next.js calls the server action with the form data automatically. No JavaScript event handler needed.

// app/create-post/page.js
import { createPost } from '@/app/actions';

export default function CreatePostPage() {
  return (
    <form action={createPost}>
      <label>
        Title
        <input name="title" type="text" required />
      </label>
      <label>
        Content
        <textarea name="content" required></textarea>
      </label>
      <button type="submit">Create Post</button>
    </form>
  );
}
DIAGRAM: Form Submission with Server Action

User fills form and clicks Submit
           ↓
Browser sends request to Next.js
           ↓
Next.js calls createPost(formData) on the server
           ↓
Server action reads form fields
           ↓
Saves data to database
           ↓
Page updates

Server Actions in Client Components

You can call server actions from client components too. Import the action and call it from an event handler or button click.

'use client';

import { useState } from 'react';
import { deletePost } from '@/app/actions';

export default function DeleteButton({ postId }) {
  const [deleting, setDeleting] = useState(false);

  async function handleDelete() {
    setDeleting(true);
    await deletePost(postId);
    setDeleting(false);
  }

  return (
    <button onClick={handleDelete} disabled={deleting}>
      {deleting ? 'Deleting...' : 'Delete'}
    </button>
  );
}
// app/actions.js
'use server';

export async function deletePost(id) {
  await db.posts.delete({ where: { id } });
}

Handling Form State with useActionState

The useActionState hook from React lets you track the result of a server action — including error messages and success states — directly in your component.

'use client';
import { useActionState } from 'react';
import { createPost } from '@/app/actions';

const initialState = { message: '', success: false };

export default function CreatePostForm() {
  const [state, formAction] = useActionState(createPost, initialState);

  return (
    <form action={formAction}>
      <input name="title" type="text" />
      <button type="submit">Create</button>
      {state.message && <p>{state.message}</p>}
    </form>
  );
}
// Updated action that returns state
'use server';

export async function createPost(prevState, formData) {
  const title = formData.get('title');

  if (!title) {
    return { message: 'Title is required', success: false };
  }

  await db.posts.create({ data: { title } });
  return { message: 'Post created!', success: true };
}

Revalidating Data After a Server Action

After a server action modifies data, you want the page to show the updated information. Use revalidatePath or revalidateTag to tell Next.js to refresh cached data for specific pages.

'use server';

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

export async function createPost(formData) {
  const title = formData.get('title');
  await db.posts.create({ data: { title } });

  revalidatePath('/blog');   // Clears cache and re-fetches /blog page data
}

Redirecting After a Server Action

'use server';

import { redirect } from 'next/navigation';
import { db } from '@/lib/db';

export async function createPost(formData) {
  const title = formData.get('title');
  const post = await db.posts.create({ data: { title } });

  redirect(`/blog/${post.id}`);   // Send user to the new post page
}

Diagram: Server Actions vs API Routes

SERVER ACTIONS:
Great for form submissions, button actions, data mutations
Less code: no fetch, no API route file, no JSON parsing
Works with or without JavaScript (progressive enhancement)

API ROUTES:
Great for public APIs, webhooks, complex logic
Necessary when you need an endpoint other apps can call
Required when you want to control the HTTP method explicitly

Key Takeaway

Server Actions are async functions marked with 'use server' that run on the server and can be called from any component. Pass them to a form's action attribute for zero-JavaScript form handling, or call them from button click handlers in client components. Use revalidatePath after mutations to keep your pages up to date, and use redirect to send users to the right page after an action completes.

Leave a Comment