next-safe-action vs ZSA: Server Actions in 2026
TL;DR
Both next-safe-action and ZSA wrap Next.js Server Actions with type safety, validation, and error handling. next-safe-action (v7, 3.5K+ GitHub stars) is the more mature option with a richer middleware system and better ecosystem adoption. ZSA is newer, smaller, and ships with a cleaner API for common patterns. For most Next.js projects using App Router, next-safe-action is the safer pick in 2026; ZSA is worth a look if you prefer a simpler mental model.
Key Takeaways
- next-safe-action v7 ships middleware chaining, metadata, client-side hooks (
useAction,useOptimisticAction), and fine-grained error handling - ZSA offers a similar feature set with a more straightforward procedural API — less boilerplate for basic actions
- Both support Zod, Valibot, and Yup for input validation
- next-safe-action has 3.5K stars and is used in production across MakerKit, ShipFast, and other popular boilerplates
- ZSA has tighter TanStack Query integration built-in (
useServerActionhooks) - Neither is officially maintained by Vercel — both are community libraries
Why You Need a Wrapper at All
Raw Next.js Server Actions have a problem: they're not type-safe at the boundary. You call an action from a form, pass some data, and there's no guarantee the data has the shape you expect. You also have to manually handle loading states, errors, optimistic updates, and authorization — every time.
The pattern of writing a server action without a wrapper looks like this:
// No validation, no error handling, no auth check
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const body = formData.get("body") as string;
// Hope the types are right...
await db.post.create({ data: { title, body } });
}
A typed wrapper gives you:
- Schema validation at the server boundary (reject bad input before it touches your DB)
- Typed return values so the client knows exactly what to expect on success or failure
- Middleware for auth checks, rate limiting, logging — applied per-action or globally
- Client hooks for loading/error/success states without manual
useStatesoup
next-safe-action v7
next-safe-action is the original and most widely adopted typed server action library for Next.js. Version 7 (released late 2025) introduced a breaking middleware API redesign that makes it much more composable.
Setup
npm install next-safe-action
// lib/safe-action.ts
import { createSafeActionClient } from "next-safe-action";
export const actionClient = createSafeActionClient();
// With auth middleware
export const authedClient = createSafeActionClient().use(async ({ next, ctx }) => {
const session = await auth();
if (!session) throw new ActionError("Unauthorized");
return next({ ctx: { userId: session.user.id } });
});
Writing an Action
// actions/create-post.ts
"use server";
import { z } from "zod";
import { authedClient } from "@/lib/safe-action";
const schema = z.object({
title: z.string().min(1).max(200),
body: z.string().min(10),
});
export const createPost = authedClient
.schema(schema)
.action(async ({ parsedInput, ctx }) => {
const post = await db.post.create({
data: {
title: parsedInput.title,
body: parsedInput.body,
authorId: ctx.userId,
},
});
return { postId: post.id };
});
Using in a Component
import { useAction } from "next-safe-action/hooks";
import { createPost } from "@/actions/create-post";
function CreatePostForm() {
const { execute, result, status } = useAction(createPost);
return (
<form action={(formData) => execute({
title: formData.get("title") as string,
body: formData.get("body") as string,
})}>
{/* ... */}
{result.serverError && <p>{result.serverError}</p>}
{status === "executing" && <Spinner />}
</form>
);
}
Error Handling
next-safe-action distinguishes between validation errors (from your schema) and server errors (thrown by your action). ActionError is the recommended way to surface user-facing errors:
import { ActionError } from "next-safe-action";
export const deletePost = authedClient
.schema(z.object({ postId: z.string() }))
.action(async ({ parsedInput, ctx }) => {
const post = await db.post.findUnique({ where: { id: parsedInput.postId } });
if (post?.authorId !== ctx.userId) {
throw new ActionError("You don't own this post");
}
await db.post.delete({ where: { id: parsedInput.postId } });
});
Middleware Chaining
The v7 middleware system is next-safe-action's standout feature. You can compose middleware clients:
// Base → auth → rate limit → logging
export const rateLimitedClient = authedClient
.use(async ({ next, ctx }) => {
await checkRateLimit(ctx.userId);
return next();
})
.use(async ({ next, clientInput }) => {
console.log("Action input:", clientInput);
return next();
});
ZSA (Zero Seconds Action)
ZSA is a newer library (2024–2025) that takes a slightly different approach. Instead of building a client with chained middleware, ZSA uses a procedural createServerAction factory with explicit procedure composition.
Setup
npm install zsa zsa-react
// lib/procedures.ts
import { createServerActionProcedure } from "zsa";
export const authedProcedure = createServerActionProcedure().handler(async () => {
const session = await auth();
if (!session) throw new Error("Unauthorized");
return { userId: session.user.id };
});
Writing an Action
// actions/create-post.ts
"use server";
import { z } from "zod";
import { authedProcedure } from "@/lib/procedures";
export const createPostAction = authedProcedure
.createServerAction()
.input(z.object({
title: z.string().min(1).max(200),
body: z.string().min(10),
}))
.handler(async ({ input, ctx }) => {
const post = await db.post.create({
data: {
title: input.title,
body: input.body,
authorId: ctx.userId,
},
});
return { postId: post.id };
});
Using in a Component
ZSA ships zsa-react with a useServerAction hook that integrates well with React:
import { useServerAction } from "zsa-react";
import { createPostAction } from "@/actions/create-post";
function CreatePostForm() {
const { execute, isPending, error } = useServerAction(createPostAction);
return (
<form onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await execute({
title: formData.get("title") as string,
body: formData.get("body") as string,
});
}}>
{/* ... */}
{error && <p>{error.message}</p>}
{isPending && <Spinner />}
</form>
);
}
TanStack Query Integration
ZSA's standout feature is first-class TanStack Query support:
import { useServerActionQuery } from "zsa-react";
// Runs on mount, refetches, caches — just like useQuery
const { data, isLoading } = useServerActionQuery(getPostsAction, {
input: { page: 1 },
queryKey: ["posts", 1],
});
This is genuinely useful for read actions that benefit from caching and background refetching.
Direct Comparison
| Feature | next-safe-action v7 | ZSA |
|---|---|---|
| GitHub Stars | 3.5K+ | 700+ |
| Validation | Zod, Valibot, Yup | Zod, Valibot |
| Middleware | Chained client API | Procedure-based |
| Client hooks | useAction, useOptimisticAction | useServerAction |
| TanStack Query | Manual integration | Built-in useServerActionQuery |
| Form integration | Native form data support | Manual |
| Metadata | ✓ per-action metadata | ✗ |
| Error types | ActionError, validation errors | Standard Error |
| Bundle size | ~4KB | ~3KB |
| Boilerplate adoption | MakerKit, ShipFast, many others | Growing |
When to Use next-safe-action
next-safe-action is the right choice when:
- You're using an existing boilerplate — MakerKit, ShipFast, and most popular Next.js starters already use next-safe-action. Switching would mean rewriting your action layer.
- You need complex middleware composition — Chaining auth, rate limiting, logging, and tenant isolation is cleaner with next-safe-action's fluent API.
- You need optimistic updates —
useOptimisticActionis well-tested and handles rollback on error. - You want broader community resources — Stack Overflow answers, blog posts, and examples are more available for next-safe-action.
When to Use ZSA
ZSA is the right choice when:
- You're starting fresh and prefer a more explicit procedural style.
- You use TanStack Query heavily — the built-in
useServerActionQueryintegration is genuinely ergonomic. - You have simpler middleware needs — ZSA's procedure system is easier to reason about for straightforward auth-only use cases.
- Your team is comfortable with the newer API — ZSA's design is clean and the DX is good.
The Honest Assessment
next-safe-action wins on maturity, ecosystem adoption, and middleware expressiveness. If you're picking up an existing project or following tutorials from popular boilerplate authors, you'll be using next-safe-action.
ZSA is genuinely competitive for greenfield projects, especially if TanStack Query is already in your stack. The useServerActionQuery pattern removes a whole class of manual wiring. But its smaller community means fewer examples and slower issue resolution.
Neither library is a wrong choice in 2026. The API surface is close enough that migrating between them later isn't catastrophic — both are thin wrappers around the same Next.js primitive.
Methodology & Sources
- next-safe-action GitHub: current v7 docs and release notes
- ZSA GitHub: current v1 docs
- npm download data: npmjs.com (April 2026)
- Feature comparison based on each library's README and TypeScript types