Server Actions

Learn how to use Next.js Server Actions for mutations in Plainform

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

On this page