npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

next-zod-action

v1.0.0

Published

Type-safe, builder-pattern interface for Next.js Server Actions powered by Zod

Readme

next-zod-action provides a type-safe, builder-pattern interface for Next.js Server Actions, powered by Zod and integrated seamlessly with react-hook-action.

Benefits

  • Type-Safe: End-to-end type inference from server validation to client hooks.
  • Input Validation: Automatic Zod validation for arguments with structured error returns.
  • Middleware Support: Powerful chainable middleware for authentication, logging, and context injection.
  • React Integration: Built-in useAction hook that manages loading states and error mapping.
  • Standardized Errors: Replaces generic Next.js "Digest" errors with a predictable success/failure envelope.
  • FormData Support: Automatically parses FormData with support for nested objects (e.g., user.name) and arrays (e.g., tags[0]).
  • Secure by Default: Automatically sanitizes unexpected server errors to prevent information leakage.
  • Metadata: Attach static metadata (e.g., permissions) to actions for smarter middleware guards.

📦 Installation

npm install next-zod-action zod

pnpm

pnpm install next-zod-action zod

yarn

yarn add next-zod-action zod

📖 Usage

Initialize Client

Create a reusable action client, typically in src/lib/action.ts. You can configure how to handle unexpected server errors (e.g., logging to Sentry).

import { createActionClient } from 'next-zod-action/server';

export const action = createActionClient({
  // Optional: runs only for unexpected crashes
  handleServerError: (e) => {
    console.error('Action Error:', e);
  },
  // Optional: custom generic message for the client
  defaultServerError: 'Something went wrong. Please try again later.',
  // Optional: automatically convert empty strings to undefined (default: true)
  normalizeFormData: true,
});

Middleware

Extend the client with middleware to inject context (like user sessions) or enforce guards. Use ActionError for messages you want the client to see.

// src/lib/action.ts
import { ActionError } from 'next-zod-action';

export const authAction = action.use(async ({ next, ctx }) => {
  const session = await getSession();

  if (!session) {
    // This message is SAFE to show to the client
    throw new ActionError('Unauthorized');
  }

  // Inject session into context for the next handler
  return next({ ctx: { ...ctx, user: session.user } });
});

Define Action

Create your server action using the builder pattern.

// src/app/actions.ts
'use server';

import { ActionError } from 'next-zod-action';
import { z } from 'zod';

import { authAction } from '@/lib/action';

const schema = z.object({
  title: z.string().min(3),
  priority: z.enum(['low', 'high']),
});

export const createTodo = authAction
  .schema(schema)
  .action(async ({ parsedInput, ctx }) => {
    // If you throw a regular Error here, the client sees "Internal Server Error"
    // If you throw an ActionError, the client sees your custom message.

    if (await isSpam(parsedInput.title)) {
      throw new ActionError('Spam detected');
    }

    const newTodo = await db.todo.create({
      data: { ...parsedInput, userId: ctx.user.id },
    });

    return { id: newTodo.id, message: 'Created!' };
  });

Consume in React

Use the useAction hook to call the action with full type safety and automatic state management.

// src/components/TodoForm.tsx
'use client';

import { useAction } from 'next-zod-action/client';

import { createTodo } from '@/app/actions';

export function TodoForm() {
  const { execute, result, validationErrors, isLoading } = useAction(
    'create-todo',
    createTodo
  );

  return (
    <form action={() => execute({ title: 'Buy Milk', priority: 'high' })}>
      <button disabled={isLoading}>
        {isLoading ? 'Saving...' : 'Create Todo'}
      </button>

      {/* Structured Validation Errors */}
      {validationErrors?.title && (
        <p className='error'>{validationErrors.title[0]}</p>
      )}

      {/* Global Error (ActionError or sanitized generic error) */}
      {result?.success === false && result.error && (
        <p className='error'>{result.error}</p>
      )}

      {/* Success State */}
      {result?.success && (
        <p className='success'>Todo created: {result.data.message}</p>
      )}
    </form>
  );
}

FormData Support

You can pass FormData directly to your action (e.g., from a native <form action={createTodo}>). The library automatically parses it into a structured object, supporting nested keys and arrays:

<form action={createTodo}>
  {/* user.name -> { user: { name: '...' } } */}
  <input name='user.name' />

  {/* tags[0] -> { tags: ['...'] } */}
  <input name='tags[0]' />
  <input name='tags[1]' />

  <button type='submit'>Submit</button>
</form>

Note: Since FormData values are always strings, use z.coerce in your schema for non-string types (e.g., z.coerce.number()).

Empty Strings: By default, empty strings "" are converted to undefined. This allows Zod's .optional() to work as expected for optional form fields. You can disable this via normalizeFormData: false.

Action Binding: The library fully supports .bind (e.g., <form action={updateTodo.bind(null, id)}>). It automatically detects FormData regardless of its position in the arguments.

Metadata

Attach static metadata to actions for permission checks in middleware.

import { ActionError } from 'next-zod-action';

const adminAction = authAction
  .metadata({ role: 'ADMIN' })
  .use(async ({ next, metadata, ctx }) => {
    if (metadata?.role && ctx.user.role !== metadata.role) {
      throw new ActionError('Forbidden');
    }
    return next({ ctx });
  });

Error Handling & Security

Standardized error handling is a core feature of next-zod-action. It ensures that your client always receives a predictable response and that sensitive server-side information never leaks.

The ActionResult Type

Every action returns a standardized ActionResult object. This allows you to handle success and failure in a uniform way across your application.

type ActionResult<TInput, TOutput> =
  | {
      success: true;
      data: TOutput;
    }
  | {
      success: false;
      error: string; // Human-readable message
      validationErrors?: { [K in keyof TInput]?: string[] } & { [key: string]: string[] };
      serverError?: boolean; // True if an exception was thrown
    };

Error Categorization

The library distinguishes between three scenarios to ensure security and a predictable developer experience:

  1. Validation Failures: Occur when the input fails the Zod schema. The error is always "Validation failed", and validationErrors contains the field-specific issues.
  2. Expected Errors (ActionError): Throw this when you want to pass a specific message back to the client. This is ideal for business logic failures (e.g., "Insufficient permissions"). These are not logged as crashes.
  3. Unexpected Errors: Any other error (e.g., database connection failure) is caught and sanitized. It returns a generic message (configurable via defaultServerError) to prevent leaking sensitive system information. These are logged to the server console.

| Scenario | success | error | validationErrors | serverError | Logged? | | :--------------------- | :-------- | :------------------------ | :----------------- | :------------ | :------ | | Success | true | — | — | — | No | | Validation Failed | false | "Validation failed" | { field: [...] } | — | No | | ActionError thrown | false | err.message | — | true | No | | Unexpected Crash | false | "Internal Server Error" | — | true | Yes |

📚 Documentation

For all configuration options, please see the API docs.

🤝 Contributing

Want to contribute? Awesome! To show your support is to star the project, or to raise issues on GitHub.

Thanks again for your support, it is much appreciated! 🙏

License

MIT © Shahrad Elahi and contributors.