We use tracking cookies to understand how you use the product and help us improve it. For more information on how we store cookies, read our  privacy policy.

Server Actions

Learn how to use Next.js Server Actions for mutations

Learn how to use Next.js Server Actions for secure server-side mutations.

Goal

By the end of this recipe, you'll have created and used Server Actions in your application.

Prerequisites

  • A working Plainform installation
  • Basic knowledge of React and Next.js

Steps

Create Server Action

Create a server action file:

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

import { auth } from '@clerk/nextjs/server';
import { prisma } from '@/lib/prisma/prisma';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const { userId } = await auth();

  if (!userId) {
    throw new Error('Unauthorized');
  }

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

  const post = await prisma.post.create({
    data: {
      title,
      content,
      authorId: userId,
    },
  });

  revalidatePath('/posts');
  return { success: true, post };
}

Always add 'use server' directive at the top of server action files.

Use in Client Component

Call the server action from a client component:

components/CreatePostForm.tsx
'use client';

import { createPost } from '@/app/actions/posts';
import { useTransition } from 'react';

export function CreatePostForm() {
  const [isPending, startTransition] = useTransition();

  const handleSubmit = async (formData: FormData) => {
    startTransition(async () => {
      const result = await createPost(formData);
      if (result.success) {
        alert('Post created!');
      }
    });
  };

  return (
    <form action={handleSubmit}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Content" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

Add Validation

Add input validation with Zod:

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

import { z } from 'zod';

const createPostSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(1).max(5000),
});

export async function createPost(formData: FormData) {
  const { userId } = await auth();

  if (!userId) {
    throw new Error('Unauthorized');
  }

  const data = {
    title: formData.get('title'),
    content: formData.get('content'),
  };

  const validated = createPostSchema.parse(data);

  const post = await prisma.post.create({
    data: {
      ...validated,
      authorId: userId,
    },
  });

  revalidatePath('/posts');
  return { success: true, post };
}

Handle Errors

Add error handling:

components/CreatePostForm.tsx
'use client';

import { useState } from 'react';

export function CreatePostForm() {
  const [isPending, startTransition] = useTransition();
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (formData: FormData) => {
    setError(null);
    startTransition(async () => {
      try {
        const result = await createPost(formData);
        if (result.success) {
          alert('Post created!');
        }
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to create post');
      }
    });
  };

  return (
    <form action={handleSubmit}>
      {error && <div className="text-red-500">{error}</div>}
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Content" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

Server Action Best Practices

Always Authenticate

Check user authentication in every server action:

const { userId } = await auth();
if (!userId) {
  throw new Error('Unauthorized');
}

Validate Input

Use Zod or similar for input validation:

const schema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});

const validated = schema.parse(data);

Revalidate Cache

Revalidate affected paths after mutations:

revalidatePath('/posts');
revalidatePath('/dashboard');

Return Serializable Data

Only return JSON-serializable data:

// Good
return { success: true, id: post.id };

// Bad - Date objects aren't serializable
return { success: true, post };

Common Issues

"use server" Missing

  • Add 'use server' directive at the top of the file
  • Ensure it's the first line (before imports)

Authentication Fails

  • Verify Clerk middleware is configured
  • Check that auth() is imported from @clerk/nextjs/server

Data Not Updating

  • Call revalidatePath() after mutations
  • Verify the path matches the page route

Next Steps

How is this guide ?

Last updated on