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 |
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:
- 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 also: Yup vs Zod and Superstruct vs Zod, tRPC v11 vs ts-rest: Type-Safe APIs in TypeScript 2026.
See the live comparison
View react 19 server action libraries on PkgPulse →