Skip to content
nextjs-server-actions logo

Next.js Server Actions

nextjs-server-actions

Complete Next.js Server Actions system (Next.js 15.5/16). PROACTIVELY activate for: (1) Defining Server Actions with 'use server', (2) Form handling with actions, (3) useActionState for form state, (4) useFormStatus for pending states, (5) Optimistic updates with useOptimistic, (6) Validation wit...

SKILL.md

Full skill instructions

Quick Reference

PatternCodePurpose
Inline actionasync function action() { 'use server'; }Define in component
Actions file'use server' at file topDedicated actions file
Form action<form action={serverAction}>Native form integration
Bind argsaction.bind(null, id)Pass additional args
HookImportPurpose
useActionStatereactForm state + pending
useFormStatusreact-domPending state in children
useOptimisticreactOptimistic UI updates
RevalidationFunctionScope
revalidatePath('/​posts')Path-basedSpecific route
revalidateTag('posts')Tag-basedAll tagged fetches
redirect('/​success')NavigationAfter mutation

When to Use This Skill

Use for server-side mutations:

  • Form submissions without API routes
  • Database mutations (create, update, delete)
  • File uploads to server
  • Implementing optimistic updates
  • Form validation with error display

Related skills:

  • For data fetching: see nextjs-data-fetching
  • For caching/​revalidation: see nextjs-caching
  • For authentication in actions: see nextjs-authentication

Next.js Server Actions

Defining Server Actions

Inline Server Action

// app/​posts/​page.tsx
export default function PostsPage() {
  async function createPost(formData: FormData) {
    'use server';

    const title = formData.get('title') as string;
    const content = formData.get('content') as string;

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

    revalidatePath('/​posts');
  }

  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Content" required />
      <button type="submit">Create Post</​button>
    </​form>
  );
}

Separate Actions File

// app/​actions.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/​cache';
import { redirect } from 'next/​navigation';
import { z } from 'zod';

const PostSchema = z.object({
  title: z.string().min(1, 'Title is required').max(100),
  content: z.string().min(1, 'Content is required'),
});

export async function createPost(formData: FormData) {
  const validatedFields = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }

  try {
    await db.posts.create({
      data: validatedFields.data,
    });
  } catch (error) {
    return { error: 'Failed to create post' };
  }

  revalidatePath('/​posts');
  redirect('/​posts');
}

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

export async function updatePost(id: string, formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await db.posts.update({
    where: { id },
    data: { title, content },
  });

  revalidatePath(`/​posts/​${id}`);
  return { success: true };
}

Using Server Actions in Forms

Basic Form

// app/​contact/​page.tsx
import { submitContact } from './​actions';

export default function ContactPage() {
  return (
    <form action={submitContact}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <button type="submit">Send</​button>
    </​form>
  );
}

Form with useActionState

// app/​signup/​page.tsx
'use client';

import { useActionState } from 'react';
import { signup } from './​actions';

const initialState = {
  errors: {} as Record<string, string[]>,
  message: '',
};

export default function SignupPage() {
  const [state, formAction, isPending] = useActionState(signup, initialState);

  return (
    <form action={formAction}>
      <div>
        <input name="email" type="email" placeholder="Email" />
        {state.errors?.email && (
          <p className="error">{state.errors.email[0]}</​p>
        )}
      </​div>

      <div>
        <input name="password" type="password" placeholder="Password" />
        {state.errors?.password && (
          <p className="error">{state.errors.password[0]}</​p>
        )}
      </​div>

      {state.message && <p className="message">{state.message}</​p>}

      <button type="submit" disabled={isPending}>
        {isPending ? 'Signing up...' : 'Sign Up'}
      </​button>
    </​form>
  );
}
// app/​signup/​actions.ts
'use server';

import { z } from 'zod';

const SignupSchema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

export async function signup(prevState: any, formData: FormData) {
  const validatedFields = SignupSchema.safeParse({
    email: formData.get('email'),
    password: formData.get('password'),
  });

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: '',
    };
  }

  try {
    await createUser(validatedFields.data);
    return { errors: {}, message: 'Account created successfully!' };
  } catch (error) {
    return { errors: {}, message: 'Failed to create account' };
  }
}

useFormStatus for Pending State

// components/​SubmitButton.tsx
'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : children}
    </​button>
  );
}
// Usage in form
import { SubmitButton } from '@/​components/​SubmitButton';

export default function Form() {
  return (
    <form action={submitForm}>
      <input name="title" />
      <SubmitButton>Submit</​SubmitButton>
    </​form>
  );
}

Optimistic Updates

useOptimistic Hook

// app/​posts/[id]/​comments.tsx
'use client';

import { useOptimistic, useTransition } from 'react';
import { addComment } from './​actions';

interface Comment {
  id: string;
  text: string;
  pending?: boolean;
}

export function Comments({ initialComments }: { initialComments: Comment[] }) {
  const [isPending, startTransition] = useTransition();
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    initialComments,
    (state, newComment: Comment) => [...state, { ...newComment, pending: true }]
  );

  async function handleSubmit(formData: FormData) {
    const text = formData.get('text') as string;

    startTransition(async () => {
      addOptimisticComment({
        id: `temp-${Date.now()}`,
        text,
      });

      await addComment(formData);
    });
  }

  return (
    <div>
      <ul>
        {optimisticComments.map((comment) => (
          <li
            key={comment.id}
            style={{ opacity: comment.pending ? 0.5 : 1 }}
          >
            {comment.text}
            {comment.pending && ' (posting...)'}
          </​li>
        ))}
      </​ul>

      <form action={handleSubmit}>
        <input name="text" placeholder="Add comment" required />
        <button type="submit" disabled={isPending}>
          Add
        </​button>
      </​form>
    </​div>
  );
}

Optimistic Like Button

// components/​LikeButton.tsx
'use client';

import { useOptimistic, useTransition } from 'react';
import { toggleLike } from '@/​app/​actions';

interface LikeButtonProps {
  postId: string;
  initialLiked: boolean;
  initialCount: number;
}

export function LikeButton({
  postId,
  initialLiked,
  initialCount,
}: LikeButtonProps) {
  const [isPending, startTransition] = useTransition();
  const [optimistic, setOptimistic] = useOptimistic(
    { liked: initialLiked, count: initialCount },
    (state, liked: boolean) => ({
      liked,
      count: liked ? state.count + 1 : state.count - 1,
    })
  );

  const handleClick = () => {
    startTransition(async () => {
      setOptimistic(!optimistic.liked);
      await toggleLike(postId);
    });
  };

  return (
    <button onClick={handleClick} disabled={isPending}>
      {optimistic.liked ? '❤️' : '🤍'} {optimistic.count}
    </​button>
  );
}

Bound Arguments

Passing Arguments with bind

// components/​DeleteButton.tsx
'use client';

import { deletePost } from '@/​app/​actions';

export function DeleteButton({ postId }: { postId: string }) {
  const deletePostWithId = deletePost.bind(null, postId);

  return (
    <form action={deletePostWithId}>
      <button type="submit">Delete</​button>
    </​form>
  );
}
// app/​actions.ts
'use server';

export async function deletePost(postId: string, formData: FormData) {
  await db.posts.delete({ where: { id: postId } });
  revalidatePath('/​posts');
}

Hidden Input Alternative

// components/​EditForm.tsx
export function EditForm({ post }: { post: Post }) {
  return (
    <form action={updatePost}>
      <input type="hidden" name="id" value={post.id} />
      <input name="title" defaultValue={post.title} />
      <textarea name="content" defaultValue={post.content} />
      <button type="submit">Update</​button>
    </​form>
  );
}

Non-Form Invocation

Button Click

// components/​LogoutButton.tsx
'use client';

import { logout } from '@/​app/​actions';

export function LogoutButton() {
  return (
    <button onClick={() => logout()}>
      Log Out
    </​button>
  );
}

Event Handler

// components/​TodoItem.tsx
'use client';

import { toggleTodo, deleteTodo } from '@/​app/​actions';

export function TodoItem({ todo }: { todo: Todo }) {
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => toggleTodo(todo.id)}
      />
      <span>{todo.title}</​span>
      <button onClick={() => deleteTodo(todo.id)}>Delete</​button>
    </​li>
  );
}

Validation

Zod Schema Validation

// app/​actions.ts
'use server';

import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  age: z.coerce.number().min(18, 'Must be at least 18 years old'),
});

export type CreateUserState = {
  errors?: {
    name?: string[];
    email?: string[];
    age?: string[];
  };
  message?: string;
};

export async function createUser(
  prevState: CreateUserState,
  formData: FormData
): Promise<CreateUserState> {
  const validatedFields = CreateUserSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    age: formData.get('age'),
  });

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }

  try {
    await db.users.create({ data: validatedFields.data });
    return { message: 'User created successfully!' };
  } catch (error) {
    return { message: 'Database error: Failed to create user.' };
  }
}

Authorization

Checking User Permissions

// app/​actions.ts
'use server';

import { auth } from '@/​lib/​auth';
import { forbidden, unauthorized } from 'next/​navigation';

export async function deletePost(postId: string) {
  const session = await auth();

  if (!session) {
    unauthorized();
  }

  const post = await db.posts.findUnique({ where: { id: postId } });

  if (post?.authorId !== session.user.id && session.user.role !== 'admin') {
    forbidden();
  }

  await db.posts.delete({ where: { id: postId } });
  revalidatePath('/​posts');
}

Role-Based Access

// app/​actions.ts
'use server';

import { auth } from '@/​lib/​auth';

export async function updateSettings(formData: FormData) {
  const session = await auth();

  if (!session) {
    return { error: 'Not authenticated' };
  }

  if (session.user.role !== 'admin') {
    return { error: 'Not authorized' };
  }

  // Update settings
  await updateSystemSettings(formData);

  revalidatePath('/​settings');
  return { success: true };
}

Error Handling

Try-Catch Pattern

// app/​actions.ts
'use server';

export async function createPost(formData: FormData) {
  try {
    const title = formData.get('title') as string;
    const content = formData.get('content') as string;

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

    revalidatePath('/​posts');
    return { success: true };
  } catch (error) {
    console.error('Failed to create post:', error);
    return { error: 'Failed to create post. Please try again.' };
  }
}

Redirect After Action

// app/​actions.ts
'use server';

import { redirect } from 'next/​navigation';

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

  revalidatePath('/​posts');
  redirect(`/​posts/​${post.slug}`);
}

File Uploads

Basic File Upload

// app/​upload/​page.tsx
import { uploadFile } from './​actions';

export default function UploadPage() {
  return (
    <form action={uploadFile}>
      <input type="file" name="file" accept="image/​*" required />
      <button type="submit">Upload</​button>
    </​form>
  );
}
// app/​upload/​actions.ts
'use server';

import { writeFile } from 'fs/​promises';
import { join } from 'path';

export async function uploadFile(formData: FormData) {
  const file = formData.get('file') as File;

  if (!file || file.size === 0) {
    return { error: 'No file provided' };
  }

  const bytes = await file.arrayBuffer();
  const buffer = Buffer.from(bytes);

  const filename = `${Date.now()}-${file.name}`;
  const path = join(process.cwd(), 'public/​uploads', filename);

  await writeFile(path, buffer);

  return { success: true, path: `/​uploads/​${filename}` };
}

Best Practices

PracticeDescription
Validate all inputsUse Zod or similar for validation
Check authorizationVerify user permissions in each action
Handle errors gracefullyReturn error messages, don't throw
Use optimistic updatesBetter UX for mutations
Revalidate properlyUse revalidatePath or revalidateTag
Keep actions focusedOne action per mutation
Type return valuesDefine state types for useActionState

More skills from JosiahSiegel

debugging-troubleshooting-2025 logo
JosiahSiegel/claude-plugin-marketplace

debugging-troubleshooting-2025

Comprehensive bash script debugging and troubleshooting techniques for 2025. PROACTIVELY activate for: (1) debugging a misbehaving bash script, (2) using set -x, set -e, set -u, set -o pipefail (strict mode), (3) customizing PS4 for richer trace output, (4) trap DEBUG, ERR, and EXIT for diagnosti...

58 0
View
tailwindcss-accessibility logo
JosiahSiegel/claude-plugin-marketplace

tailwindcss-accessibility

Tailwind CSS accessibility patterns including WCAG 2.2 compliance, touch targets, focus management, and ARIA. PROACTIVELY activate for: (1) building accessible UI with Tailwind, (2) WCAG 2.2 compliance (Level A, AA, AAA), (3) focus rings and focus-visible styling, (4) minimum touch-target sizes (...

58 0
View
sql-server-2025 logo
JosiahSiegel/claude-plugin-marketplace

sql-server-2025

SQL Server 2025 and SqlPackage 170.2.70 (October 2025) — vector databases, AI integration, and modern features. PROACTIVELY activate for: (1) SQL Server 2025 features, (2) vector type and vector indexes for AI, (3) Vector functions (VECTOR_DISTANCE, etc.), (4) AI integration (Azure OpenAI, embedd...

58 0
View
adf-master logo
JosiahSiegel/claude-plugin-marketplace

adf-master

Azure Data Factory (ADF) CI/CD, deployment, and pipeline development. PROACTIVELY activate for: (1) ADF CI/CD setup (npm validation, ARM template export), (2) ADF ARM template deployment, (3) ADF npm build validation in CI, (4) PrePostDeploymentScript for trigger and resource cleanup, (5) ADF Git...

58 0
View
powershell-security logo
JosiahSiegel/claude-plugin-marketplace

powershell-security

Modern PowerShell security practices for 2025 — SecretManagement, JEA, WDAC, credential protection. PROACTIVELY activate for: (1) Microsoft.PowerShell.SecretManagement and SecretStore for credential storage, (2) Just Enough Administration (JEA) endpoints, (3) Windows Defender Application Control ...

58 0
View
nextjs-middleware logo
JosiahSiegel/claude-plugin-marketplace

nextjs-middleware

Complete Next.js Middleware system (Next.js 15.5/16). PROACTIVELY activate for: (1) Creating middleware.ts (Edge) or proxy.ts (Node.js), (2) Matcher configuration, (3) Authentication protection, (4) Role-based access control, (5) Redirects and rewrites, (6) Internationalization (i18n), (7) Securi...

58 0
View
nextjs-data-fetching logo
JosiahSiegel/claude-plugin-marketplace

nextjs-data-fetching

Complete Next.js data fetching system (Next.js 15.5/16). PROACTIVELY activate for: (1) Server Component data fetching, (2) Parallel and sequential fetching, (3) Streaming with Suspense, (4) Route Handlers (API routes), (5) Client-side fetching with SWR/TanStack Query, (6) generateStaticParams for...

58 0
View
advanced-features-2025 logo
JosiahSiegel/claude-plugin-marketplace

advanced-features-2025

Advanced Claude Code plugin features for hooks, MCP integration, team distribution, and progressive disclosure. PROACTIVELY activate for: (1) progressive disclosure design, (2) hook automation, (3) PreToolUse and PostToolUse hooks, (4) MCP server integration, (5) .mcp.json configuration, (6) team...

58 0
View
azure-sql-optimization logo
JosiahSiegel/claude-plugin-marketplace

azure-sql-optimization

Azure SQL Database optimization and platform-specific features. PROACTIVELY activate for: (1) Azure SQL Database optimization, (2) DTU vs vCore selection and right-sizing, (3) automatic tuning (force last good plan, create/drop index), (4) Hyperscale tier and read replicas, (5) Serverless tier an...

58 0
View
github-actions-2025 logo
JosiahSiegel/claude-plugin-marketplace

github-actions-2025

GitHub Actions 2025-2026 features and modernization. PROACTIVELY activate for: (1) 1 vCPU Linux runners (October 2025 public preview), (2) immutable releases for hardened distribution, (3) Node24 migration from Node20, (4) reusable workflows and composite actions, (5) workflow_dispatch with rich ...

58 0
View
git-2-49-features logo
JosiahSiegel/claude-plugin-marketplace

git-2-49-features

Git 2.49 specific features and performance improvements. PROACTIVELY activate for: (1) git-backfill command (new in 2.49) for partial clones, (2) path-walk API improvements, (3) zlib-ng integration for faster compression, (4) reftables backend stabilization, (5) revision-walk performance, (6) bun...

58 0
View
vitest-3-features logo
JosiahSiegel/claude-plugin-marketplace

vitest-3-features

Vitest 3 features and migration from Vitest 2 (2025). PROACTIVELY activate for: (1) Vitest 3 stable features, (2) workspace mode for monorepos, (3) test annotations and reporter API, (4) projects configuration, (5) browser mode in Vitest 3, (6) inline snapshots and toMatchInlineSnapshot, (7) type...

58 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View