Skip to main content

next-safe-action vs ZSA: Server Actions in 2026

·PkgPulse Team
0

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 (useServerAction hooks)
  • 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 useState soup

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

Featurenext-safe-action v7ZSA
GitHub Stars3.5K+700+
ValidationZod, Valibot, YupZod, Valibot
MiddlewareChained client APIProcedure-based
Client hooksuseAction, useOptimisticActionuseServerAction
TanStack QueryManual integrationBuilt-in useServerActionQuery
Form integrationNative form data supportManual
Metadata✓ per-action metadata
Error typesActionError, validation errorsStandard Error
Bundle size~4KB~3KB
Boilerplate adoptionMakerKit, ShipFast, many othersGrowing

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 updatesuseOptimisticAction is 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 useServerActionQuery integration 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

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.