next-safe-action
Type safe and validated Server Actions for Next.js, with typed errors, middleware, and React hooks for execution state.
Repository Health
Technical Analysis
next-safe-action wraps Next.js Server Actions in a fluent, chainable client builder that adds input/output validation, typed error handling, and composable middleware on top of the raw Server Actions API. Instead of hand-rolling validation and error shaping inside every action, you define a createSafeActionClient() once, attach middleware and a validation schema (any Standard Schema v1 implementation, such as Zod, Valibot, or Yup), and get a fully typed action function whose inputs, outputs, and error shapes are inferred end-to-end from server to client.
On the client, a set of React hooks (useAction, useOptimisticAction, useStateAction, useOptimisticStateAction) track execution status, expose typed results, and integrate with React’s useActionState/optimistic UI primitives, so components can react to pending, success, validation-error, and server-error states without manually wiring useState/useTransition around every call. A two-tier middleware system separates plain middleware (runs before validation) from validated middleware (runs after validation, with typed access to parsedInput), following an onion model where code before next() runs pre-action and code after next() sees the result.
What You Get
- A chainable
createSafeActionClient()builder for defining reusable action clients with shared middleware, metadata schemas, and error handling - Input, bind-args, and output validation via any Standard Schema v1 library (Zod, Valibot, Yup, and others) with no library-specific adapter required
- A two-tier middleware system:
use()for pre-validation middleware anduseValidated()for middleware with typed access to parsed input, following an onion (before/afternext()) execution model - React hooks (
useAction,useStateAction,useOptimisticAction,useOptimisticStateAction) that track idle/executing/hasSucceeded/hasErrored status and expose typed data, validation errors, and server errors - Typed server-error handling via
handleServerError/returnServerError, and typed validation-error shaping viareturnValidationErrors,flattenValidationErrors, andformatValidationErrors - Automatic detection and re-throwing of Next.js framework/navigation errors (
redirect,notFound,forbidden,unauthorized) so they aren’t swallowed by the action’s own error handling
Common Use Cases
- Defining mutation endpoints for a Next.js App Router app (form submissions, CRUD operations) with schema-validated input and typed success/error results
- Adding cross-cutting concerns (authentication checks, rate limiting, logging, request-scoped context) to many actions via a shared middleware chain instead of repeating boilerplate in each action
- Building forms that need pending/optimistic UI:
useOptimisticActionanduseOptimisticStateActionlet a form show an optimistic result while the server action is still in flight - Returning structured, typed validation errors to the client (field-level messages) instead of a single generic error string, using
returnValidationErrorsor a customhandleValidationErrorsShape - Progressively adopting stricter typing in an existing Server Actions codebase, since
createSafeActionClient()instances can be composed and each.use()/.inputSchema()call narrows the resulting action’s type
Under The Hood
Architecture
The library is built around an immutable builder chain: createSafeActionClient() constructs a SafeActionClient instance holding an internal args object (middleware functions, schemas, error handlers), and every instance method (use, useValidated, inputSchema, bindArgsSchemas, outputSchema, metadata) returns a new SafeActionClient with an updated args object rather than mutating the original, which is what lets each call narrow the TypeScript generics for the next link in the chain. The actual runtime work is delegated to actionBuilder() (src/action-builder.ts), which closes over the accumulated args and returns action/stateAction factories. At call time it runs two distinct middleware stacks in sequence — a pre-validation stack (use()) and a post-validation stack (useValidated()) — each implemented as a recursive executeMiddlewareStack(idx) / executeValidatedMiddlewareStack(idx, ...) function that calls itself with idx + 1 inside each middleware’s next(), giving an onion model where ctx accumulates via deepmerge as the recursion unwinds. A middlewareResult object and a FrameworkErrorHandler are threaded through the whole call as mutable state, and buildResultAndRunCallbacks() applies a fixed precedence (validationErrors > serverError > data) to collapse that mutable state into one discriminated result object at the end.
Tech Stack
The package is pure TypeScript (tsdown for building dual ESM output with .d.mts declarations, oxlint for type-aware linting, vitest for tests) with zero runtime dependencies beyond what the consumer supplies — validation is delegated entirely to the Standard Schema v1 interface (~standard.validate) so Zod, Valibot, and Yup are all supported through one code path (standard-schema.ts) rather than per-library adapters. It’s published from a pnpm/Turborepo monorepo (next-safe-action-monorepo) alongside sibling packages (adapter-better-auth, adapter-react-hook-form, adapter-tanstack-query) and a docs/playground app, using Changesets for versioned releases. peerDependencies require Next.js >= 14 and React >= 18.2, with React hooks (hooks.ts) built on useActionState/transitions rather than a custom state machine.
Code Quality
The package ships 28 test files under src/__tests__/ covering happy paths, validation-error shapes, middleware edge cases (including a dedicated middleware-edge-cases and validated-middleware-edge-cases suite), navigation-error handling, optimistic/stateful hooks, and race conditions in the hook layer — a notably thorough suite for a library of this size, run via vitest with jsdom for the hook tests. tsconfig.json enables strict, noUncheckedIndexedAccess, noImplicitReturns, and noFallthroughCasesInSwitch, and the codebase favors explicit typed error classes (ActionValidationError, ActionBindArgsValidationError, ActionOutputDataValidationError) over thrown strings. Inline comments in action-builder.ts explicitly document non-obvious ordering decisions (e.g. why validation-error precedence must be checked before a server-error guard), which is a good sign for long-term maintainability. A GitHub Actions CI workflow (ci.yml) plus separate release and preview-release workflows gate merges.
API Design
The public API is comfortable to start with (createSafeActionClient() → .inputSchema(zodSchema) → .action(fn)) but backs that simplicity with heavy conditional-generic typing so that chaining use(), inputSchema(), and useValidated() in the wrong order is caught at compile time (e.g. use() typed as unusable once useValidated() has been called, via a this parameter constraint rather than a runtime check alone). Error handling is deliberately layered: returnServerError() for expected typed errors, handleServerError for a catch-all, and returnValidationErrors/flattenValidationErrors/formatValidationErrors for shaping field-level validation output — giving consumers escape hatches without forcing everyone through one error shape. Documentation lives on a dedicated docs site (next-safe-action.dev) rather than in the README, which keeps the README itself terse but pushes newcomers off-repo for anything beyond installation.
Used by 4 apps in this directory
Dub
Marketing · Analytics
The open-source link attribution platform for short links, conversion tracking, and affiliate programs — powering 100M+ clicks monthly.
Formbricks
Forms Surveys · Marketing · Analytics
Open-source experience management platform for in-app, website, email, and link surveys — privacy-first and fully self-hostable.
Midday
Invoicing Finance · Productivity
All-in-one AI-powered business operations platform for freelancers and solo entrepreneurs to manage invoicing, time tracking, banking, and financial intelligence.
Rallly
Scheduling
Self-hostable group scheduling polls that eliminate back-and-forth emails and find the best meeting time for everyone.