Skip to main content

Best React 19 Server Action Libraries 2026

·PkgPulse Team

React 19's server actions shipped production-ready, but raw server actions have a problem: there's no built-in validation, no type-safe error handling, and no standardized way to handle authentication middleware. In a typical Next.js app, you'd add these yourself for every action. That's exactly the gap that next-safe-action and zsa fill — and they're being adopted fast.

TL;DR

next-safe-action is the most mature and widely adopted server action library for Next.js, with excellent middleware support for auth/rate-limiting. ZSA (Zod Server Actions) is a solid alternative with similar capabilities and a slightly different API style. For form handling specifically, Conform works well alongside either. For new projects, either next-safe-action or ZSA is a significant improvement over raw server actions.

Key Takeaways

  • Raw React 19 server actions lack validation, type-safe errors, and middleware out of the box
  • next-safe-action: battle-tested library for typed, validated, middleware-wrapped server actions in Next.js
  • zsa: alternative with similar functionality plus React Query integration
  • Both use Zod (or similar schema libraries) for input/output validation
  • conform: handles progressively-enhanced forms with server action integration
  • Server actions with these libraries give you end-to-end type safety from form → action → response
  • Middleware pattern enables auth, rate limiting, tenant isolation in a composable way

The Problem with Raw Server Actions

React 19 server actions are powerful but verbose when used safely:

// Raw server action — NOT production-ready
'use server';

export async function createPost(formData: FormData) {
  // Manual auth check
  const session = await getServerSession();
  if (!session?.user) throw new Error('Unauthorized');

  // Manual validation
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;
  if (!title || title.length < 3) return { error: 'Title too short' };
  if (!content) return { error: 'Content required' };

  // Manual error handling
  try {
    await db.post.create({ data: { title, content, authorId: session.user.id } });
    revalidatePath('/posts');
    return { success: true };
  } catch (e) {
    return { error: 'Failed to create post' };
  }
}

This is 20 lines for a simple action. Every action needs the same auth check, validation pattern, and error handling. Libraries abstract this away.

next-safe-action

Package: next-safe-action GitHub: TheEdoRan/next-safe-action GitHub stars: 3.5K

Setup

npm install next-safe-action zod
// lib/safe-action.ts — Define your action client
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';

// Basic client
export const actionClient = createSafeActionClient();

// Client with middleware (auth, logging, rate limiting)
export const authedActionClient = createSafeActionClient()
  .use(async ({ next, clientInput, metadata }) => {
    // Middleware runs before every action
    const session = await getServerSession();
    if (!session?.user) throw new ActionError('Unauthorized');
    return next({ ctx: { userId: session.user.id } });
  })
  .use(async ({ next, ctx }) => {
    // Chained middleware: rate limiting
    await rateLimit(ctx.userId, '10/minute');
    return next({ ctx });
  });

Defining Actions

// actions/post.ts
'use server';
import { authedActionClient } from '@/lib/safe-action';
import { z } from 'zod';

const createPostSchema = z.object({
  title: z.string().min(3, 'Title must be at least 3 characters').max(100),
  content: z.string().min(10, 'Content must be at least 10 characters'),
  tags: z.array(z.string()).optional(),
});

export const createPost = authedActionClient
  .schema(createPostSchema)
  .action(async ({ parsedInput, ctx }) => {
    // parsedInput is fully typed: { title: string, content: string, tags?: string[] }
    // ctx.userId is available from middleware
    const post = await db.post.create({
      data: {
        ...parsedInput,
        authorId: ctx.userId,
      },
    });
    revalidatePath('/posts');
    return { post };
  });

Using Actions in Components

'use client';
import { useAction } from 'next-safe-action/hooks';
import { createPost } from '@/actions/post';

function CreatePostForm() {
  const { execute, result, status } = useAction(createPost);

  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    execute({
      title: formData.get('title') as string,
      content: formData.get('content') as string,
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" placeholder="Post title" />
      <textarea name="content" placeholder="Content" />

      {result.validationErrors && (
        <ul>
          {Object.entries(result.validationErrors).map(([field, errors]) => (
            <li key={field}>{field}: {errors?._errors?.join(', ')}</li>
          ))}
        </ul>
      )}

      {result.serverError && <p>Error: {result.serverError}</p>}
      {result.data?.post && <p>Post created!</p>}

      <button type="submit" disabled={status === 'executing'}>
        {status === 'executing' ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

Output Validation

export const getUser = authedActionClient
  .schema(z.object({ userId: z.string() }))
  .outputSchema(z.object({
    id: z.string(),
    name: z.string(),
    email: z.string().email(),
  }))
  .action(async ({ parsedInput, ctx }) => {
    const user = await db.user.findUnique({ where: { id: parsedInput.userId } });
    if (!user) throw new ActionError('User not found');
    return user; // Validated against outputSchema at runtime
  });

ZSA (Zod Server Actions)

Package: zsa, zsa-react GitHub: IdoPesok/zsa GitHub stars: 1.5K

ZSA takes a very similar approach to next-safe-action with some different API choices:

npm install zsa zsa-react zod

Defining Actions with ZSA

// actions/post.ts
'use server';
import { createServerAction, createServerActionProcedure } from 'zsa';
import { z } from 'zod';

// Procedure = reusable middleware
const authedProcedure = createServerActionProcedure()
  .handler(async () => {
    const session = await getServerSession();
    if (!session?.user) throw new Error('Unauthorized');
    return { userId: session.user.id };
  });

export const createPostAction = authedProcedure
  .createServerAction()
  .input(z.object({
    title: z.string().min(3).max(100),
    content: z.string().min(10),
  }))
  .output(z.object({
    post: z.object({ id: z.string(), title: z.string() }),
  }))
  .handler(async ({ input, ctx }) => {
    const post = await db.post.create({
      data: { ...input, authorId: ctx.userId },
    });
    revalidatePath('/posts');
    return { post };
  });

ZSA with React Query

ZSA has first-class React Query integration:

'use client';
import { useServerActionQuery, useServerActionMutation } from 'zsa-react';
import { getPostsAction, createPostAction } from '@/actions/post';

function PostsList() {
  const { data, isPending } = useServerActionQuery(getPostsAction, {
    input: { page: 1 },
    queryKey: ['posts'],
  });

  return isPending ? <Spinner /> : <List items={data.posts} />;
}

function CreatePostMutation() {
  const { mutate, isPending, isError, error } = useServerActionMutation(createPostAction, {
    onSuccess: () => {
      toast.success('Post created!');
    },
  });

  return (
    <button onClick={() => mutate({ title: 'New Post', content: 'Content here' })}>
      {isPending ? 'Creating...' : 'Create'}
    </button>
  );
}

Conform: Progressive Enhancement for Forms

Package: @conform-to/react, @conform-to/zod Weekly downloads: ~200K

Conform focuses specifically on form handling with server actions, emphasizing progressive enhancement (forms work without JavaScript):

npm install @conform-to/react @conform-to/zod zod
// actions/post.ts
'use server';
import { parseWithZod } from '@conform-to/zod';
import { z } from 'zod';

const schema = z.object({
  title: z.string().min(3),
  content: z.string().min(10),
});

export async function createPost(prevState: unknown, formData: FormData) {
  const submission = parseWithZod(formData, { schema });

  if (submission.status !== 'success') {
    return submission.reply(); // Returns structured validation errors
  }

  await db.post.create({ data: submission.value });
  redirect('/posts');
}
// CreatePostForm.tsx
'use client';
import { useForm } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { useFormState } from 'react-dom';
import { createPost } from '@/actions/post';

export function CreatePostForm() {
  const [lastResult, action] = useFormState(createPost, undefined);
  const [form, fields] = useForm({
    lastResult,
    onValidate({ formData }) {
      return parseWithZod(formData, { schema });
    },
    shouldValidate: 'onBlur',
    shouldRevalidate: 'onInput',
  });

  return (
    <form id={form.id} onSubmit={form.onSubmit} action={action} noValidate>
      <input key={fields.title.key} name={fields.title.name} />
      {fields.title.errors && <p>{fields.title.errors[0]}</p>}

      <textarea key={fields.content.key} name={fields.content.name} />
      {fields.content.errors && <p>{fields.content.errors[0]}</p>}

      <button type="submit">Create Post</button>
    </form>
  );
}

Conform's strength: it handles nested forms, field arrays, and file uploads with progressive enhancement that most other libraries don't support.

Feature Comparison

FeatureRaw Actionsnext-safe-actionZSAConform
Input validationManualZod (built-in)Zod (built-in)Zod
Output validationNoYesYesNo
Middleware/authManualYes (pipelines)Yes (procedures)No
Type safetyPartialFull end-to-endFull end-to-endFull
React Query integrationNoNoYesNo
Progressive enhancementNoNoNoYes
React Hook FormWorksWorksWorksSeparate
useAction hookNoYesuseServerActionuseFormState

The Standard Stack in 2026

Most Next.js projects in 2026 use this combination:

Form Library: React Hook Form or Conform
Validation: Zod
Server Actions: next-safe-action or ZSA
Data Fetching: TanStack Query or SWR (for client-side)
UI: Shadcn UI (uses Radix primitives)

With this stack:

  1. Define your Zod schema once
  2. Use it in both client-side form validation AND server-side input validation
  3. Get full TypeScript types from form inputs through to database operations

Which to Choose

Choose next-safe-action if:

  • You want the most mature, battle-tested option
  • Complex auth middleware (multi-tenant, RBAC) is needed
  • You prefer a stable, established API

Choose ZSA if:

  • React Query integration is important
  • You like ZSA's procedure-based middleware pattern
  • You want output validation in addition to input validation

Choose Conform if:

  • Progressive enhancement (works without JS) is a requirement
  • Nested forms or field arrays are needed
  • You prefer the native formData flow

Use raw server actions if:

  • Actions are very simple (one-off, no auth, no validation needed)
  • You're prototyping and don't need production-grade tooling

Compare these libraries on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.