MyApp

Getting Started

Introduction

Content & Marketing

Add Blog PostAdd Blog AuthorAdd TestimonialsCustomize HeroAdd FAQ Items

Payments

Add Stripe ProductCreate Stripe SubscriptionAdd Stripe CouponCustomize CheckoutTest Payments Locally

Authentication

Customize Sign-InAdd OAuthImplement RolesProtect Routes

Content Management

Add Doc PageCreate Doc SectionCustomize Theme

Customization

Change ColorsAdd FontCustomize EmailsUse PostHog Analytics

Deployment

Deploy VercelDatabase Migrations

Advanced

Server ActionsAdd Rate Limiting
MyApp

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

  • Add Rate Limiting - Protect your API routes

Related Documentation

  • Server Actions - Next.js Server Actions guide
  • Form Patterns - Form handling patterns

How is this guide ?

Last updated on

Database Migrations

Learn how to run database migrations in production

Add Rate Limiting

Learn how to add rate limiting to protect your API routes

On this page

Goal
Prerequisites
Steps
Create Server Action
Use in Client Component
Add Validation
Handle Errors
Server Action Best Practices
Always Authenticate
Validate Input
Revalidate Cache
Return Serializable Data
Common Issues
"use server" Missing
Authentication Fails
Data Not Updating
Next Steps
Related Documentation