<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/next-safe-action-vs-zsa-type-safe-server-actions-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/next-safe-action-vs-zsa-type-safe-server-actions-2026/raw.md -->
<!-- Source path: content/guides/next-safe-action-vs-zsa-type-safe-server-actions-2026.mdx -->

---
og_image: "/images/guides/next-safe-action-vs-zsa-type-safe-server-actions-2026.webp"
title: "next-safe-action vs ZSA: Server Actions in 2026"
description: "next-safe-action v7 vs ZSA compared: middleware, validation, error handling, and DX for type-safe Next.js server actions in 2026. Which library should you pick?"
date: "2026-04-13"
authors: ["team"]
tier: 1
tags: ["next-safe-action", "zsa", "server-actions", "nextjs", "typescript", "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 (`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:

```typescript
// 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

```bash
npm install next-safe-action
```

```typescript
// 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

```typescript
// 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

```typescript
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:

```typescript
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:

```typescript
// 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

```bash
npm install zsa zsa-react
```

```typescript
// 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

```typescript
// 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:

```typescript
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:

```typescript
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** — `useOptimisticAction` 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

### Internal Links

- [Best React 19 Server Action Libraries](/guides/best-react-19-server-action-libraries-2026)
- [tRPC v11 vs ts-rest Type-Safe API Clients](/guides/hono-rpc-vs-trpc-vs-ts-rest-type-safe-api-clients-2026)
- [TanStack Form vs React Hook Form vs Conform](/guides/tanstack-form-vs-react-hook-form-vs-conform-react-forms-2026)
