shahradelahi/next-zod-action(新しいタブで開きます)
主要言語: TypeScript
⚡ Type-safe, builder-pattern Next.js Server Actions powered by Zod.
- star 数
- 5
- watcher 数
- 1
- fork 数
- 0
- issue 数
- 5
- 最終更新
- 2026/08/20
主要言語: TypeScript
⚡ Type-safe, builder-pattern Next.js Server Actions powered by Zod.
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.
useAction hook that manages loading states and error mapping.FormData with support for nested objects (e.g., user.name) and arrays (e.g., tags[0]).npm install next-zod-action zod
pnpm
pnpm install next-zod-action zod
yarn
yarn add next-zod-action zod
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, });
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 } }); });
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!' }; });
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> ); }
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
FormDatavalues are always strings, usez.coercein your schema for non-string types (e.g.,z.coerce.number()).Empty Strings: By default, empty strings
""are converted toundefined. This allows Zod's.optional()to work as expected for optional form fields. You can disable this vianormalizeFormData: false.Action Binding: The library fully supports
.bind(e.g.,<form action={updateTodo.bind(null, id)}>). It automatically detectsFormDataregardless of its position in the arguments.
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 }); });
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.
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 };
The library distinguishes between three scenarios to ensure security and a predictable developer experience:
error is always "Validation failed", and validationErrors contains the field-specific issues.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.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 |
For all configuration options, please see the API docs.
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! 🙏
MIT © Shahrad Elahi and contributors.