Skip to main content

Best React 19 Server Action Libraries 2026

·PkgPulse Team
0

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

Security Considerations for Server Actions

Server actions introduce a security surface that's easy to underestimate. Because server actions are callable from any client — including directly via HTTP POST with crafted payloads — they must be treated as untrusted input endpoints equivalent to REST API routes. The critical difference from REST routes is that server actions don't require explicit authentication middleware configuration per route; it's the developer's responsibility to add auth checks. next-safe-action's middleware system addresses this by making auth the default for authedActionClient — any action created with this client requires a valid session before the resolver runs. Without a library like next-safe-action or ZSA, there's no framework-level enforcement of auth on individual actions, and developers occasionally forget the session check on new actions. Rate limiting is equally important: server actions are designed for forms and buttons but can be triggered programmatically, making API-level rate limiting (via middleware that checks a Redis counter per user) necessary for actions that perform expensive operations or send emails.

TypeScript End-to-End Type Safety

The type safety story is the core argument for these libraries over raw server actions. next-safe-action's createSafeActionClient() produces an action client where calling .schema(zodSchema) narrows the parsedInput type in the resolver to exactly what Zod infers from the schema. The return type of the action is a discriminated union: { data: T } | { serverError: string } | { validationErrors: ZodError } — giving client-side code precise TypeScript types for each possible outcome without any any escapes. ZSA similarly types the handler's input and the useServerAction hook's returned data and error fields. This end-to-end typing means refactoring a Zod schema on the server immediately surfaces as TypeScript errors in the client component that uses that action — the same feedback loop tRPC provides, but for the specific case of form-driven mutations in Next.js. Compare this to raw server actions where the return type is Promise<any> and client code must cast or narrow manually.

Middleware Composition and Multi-Tenant Patterns

The middleware composability of next-safe-action and ZSA enables complex authorization patterns that scale to enterprise requirements. A multi-tenant SaaS application needs to verify the user's session, resolve their current organization from the session, and verify the user has permission to act within that organization — all before the resolver runs. With next-safe-action's chained middleware, this composes naturally: one middleware validates the session, the next resolves the organization, the next checks the role, and the resolver receives a fully typed context with { userId, orgId, role } already resolved. ZSA's procedure pattern achieves the same result. Conform's middleware story is weaker — it focuses on form parsing and validation but leaves auth and rate limiting to the raw server action function. For applications where complex auth middleware is a first-class concern (RBAC, multi-tenancy, audit logging), next-safe-action or ZSA's composable middleware is architecturally superior to building those concerns into each raw action individually.

Error Handling Patterns and User Experience

The error handling model shapes how users experience validation failures and server errors. Raw server actions return undefined on success or a thrown error on failure — client code must catch errors and map them to UI state manually, with no standard shape for validation errors versus server errors. next-safe-action standardizes the response shape: result.data contains success data, result.validationErrors contains Zod field-level errors, and result.serverError contains sanitized error messages. This enables consistent error display patterns across all forms in an application — a shared FormErrors component that reads from result.validationErrors works for every action using authedActionClient. ZSA's error handling follows the same discriminated union pattern with slightly different property names. Conform takes a different approach: the submission.reply() method serializes errors in a format that Conform's React hooks can render into field-level error messages, supporting the progressive enhancement model where the same form works without JavaScript using standard HTML form behavior and native error display.

Output Validation and Data Contracts

Output validation — validating what the server action returns, not just what it receives — is a feature that both next-safe-action and ZSA support but that's underutilized in practice. The security benefit is preventing accidental data leakage: a resolver that queries the database might inadvertently include sensitive fields (passwordHash, internalNotes, billingEmail) unless the return object is explicitly shaped. An outputSchema strips undeclared fields and throws if required output fields are missing, functioning as a data contract that the action must fulfill. This pattern is particularly valuable when the database model (Prisma's User) includes more fields than the client should receive, eliminating the need to remember to .select() only safe fields on every query. The runtime overhead of output validation is minimal — a Zod schema parse on the output object adds microseconds, not milliseconds. Teams that adopt output validation report that it catches data leaks during development rather than in security reviews.

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.

See also: Yup vs Zod and Superstruct vs Zod, tRPC v11 vs ts-rest: Type-Safe APIs in TypeScript 2026.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.