Best React 19 Server Action Libraries 2026
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.jszsa: 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
| Feature | Raw Actions | next-safe-action | ZSA | Conform |
|---|---|---|---|---|
| Input validation | Manual | Zod (built-in) | Zod (built-in) | Zod |
| Output validation | No | Yes | Yes | No |
| Middleware/auth | Manual | Yes (pipelines) | Yes (procedures) | No |
| Type safety | Partial | Full end-to-end | Full end-to-end | Full |
| React Query integration | No | No | Yes | No |
| Progressive enhancement | No | No | No | Yes |
| React Hook Form | Works | Works | Works | Separate |
| useAction hook | No | Yes | useServerAction | useFormState |
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:
- Define your Zod schema once
- Use it in both client-side form validation AND server-side input validation
- 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
formDataflow
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 the live comparison
View react 19 server action libraries on PkgPulse →