nuxt-safe-action
| Install | |
|---|---|
npm install nuxt-safe-action |
|
| Latest Version: | 0.8.1 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 24, 2026 |
| Links: | GitHub · npm |
nuxt-safe-action
Type-safe server actions for Nuxt, with validated contracts and predictable form errors.
Define actions in server/actions/, compose typed middleware, and import generated references into Vue components. Use Zod, Valibot, ArkType, Yup, or another Standard Schema v1 validator. Yup requires version 1.7 or newer. Existing Zod 3 and 4 actions keep working.
Documentation · Runnable example · StackBlitz · Changelog
Advanced forms and mutations
Version 0.7 adds declared business failures, nested form state, and successful-mutation refresh of ordinary Nuxt queries. Existing builder methods, error envelopes and basic form bindings remain compatible.
const action = createSafeActionClient()
.schema(profileSchema)
.errors({ EMAIL_TAKEN: 'This email is already registered' })
.action(({ parsedInput, fail }) => {
if (parsedInput.profile.email === 'taken@example.com') {
return fail('EMAIL_TAKEN', { validationErrors: { 'profile.email': ['Use a different email'] } })
}
return { profile: parsedInput.profile }
})
useFormAction adds typed field getters/setters, dirty/touched tracking, and optional client validation while preserving raw submission snapshots and newer edits. toFormErrors() adapts dotted errors to { name, message } UI messages, keeping root errors separate.
Register an ordinary useAsyncData key with useActionQueryTags(key, ['profiles']), then use invalidate: { keys, tags } on a mutation. Successful latest executions refresh matching Nuxt keys; failures, stale requests, and cancellation do not. Refresh errors stay separate from successful writes. Nuxt continues to own caching and hydration.
Advanced runnable profile form · Forms · Business errors · Query refresh
Quick setup
npx nuxi module add nuxt-safe-action
pnpm add zod
# Alternatives: pnpm add valibot, pnpm add arktype, or pnpm add yup@^1.7.0
Or install manually:
pnpm add nuxt-safe-action zod
// nuxt.config.ts
export default defineNuxtConfig({ modules: ['nuxt-safe-action'] })
The module requires a Nuxt server deployment for actions. A static-only export cannot execute them. Nuxt 3.8+ and Nuxt 4 are supported; use the Node version required by your Nuxt release.
// server/utils/action-client.ts
import { createSafeActionClient } from '#safe-action'
export const actionClient = createSafeActionClient({
handleServerError(error) {
console.error('Action failed:', error)
return 'Something went wrong. Please try again.'
},
})
// server/actions/create-post.ts
import { z } from 'zod'
import { actionClient } from '../utils/action-client'
export default actionClient
.schema(z.object({ title: z.string().trim().min(1).max(200) }))
.outputSchema(z.object({ id: z.string(), title: z.string() }))
.action(({ parsedInput }) => ({
id: crypto.randomUUID(),
title: parsedInput.title,
}))
// Demonstration response; connect your own persistence in the handler.
<script setup lang="ts">
import { createPost } from '#safe-action/actions'
const { values, submit, data, fieldErrors, rootErrors, serverError, isExecuting } =
useFormAction(createPost, { initialValues: { title: '' } })
</script>
<template>
<form @submit.prevent="submit()">
<label for="title">Title</label>
<input id="title" v-model="values.title" :aria-invalid="Boolean(fieldErrors.title)" />
<p v-for="message in fieldErrors.title" :key="message">{{ message }}</p>
<p v-for="message in rootErrors" :key="message" role="alert">{{ message }}</p>
<p v-if="serverError" role="alert">{{ serverError }}</p>
<button :disabled="isExecuting">{{ isExecuting ? 'Creating…' : 'Create' }}</button>
<p v-if="data">Created: {{ data.title }}</p>
</form>
</template>
Features
- Input and output inference through generated client references, including schema transforms.
- Standard Schema v1 validation, asynchronous refinements, and legacy Zod 3.20+ parsing.
- Immutable builder chains, metadata, H3 request access, and typed middleware context.
- Nested field errors, root errors, intentional
ActionErrormessages, and generic unexpected failures. useActionwith latest-execution state ownership, cancellation, timeouts, headers, and callbacks.useFormActionwith typed values, submission snapshots, and reset.useOptimisticActionwith guarded rollback and application-controlled reconciliation.- POST by default, GET/POST/PUT/PATCH/DELETE filename suffixes, nested directories, and development route regeneration.
Choose the right abstraction
| Approach | Integration | Good fit |
|---|---|---|
Nuxt $fetch / useFetch |
Ordinary Nitro routes; choose your validation/error contract | Direct HTTP APIs or existing endpoints |
nuxt-safe-action |
Dedicated action directory, typed builder, consistent result envelope | Validated forms and mutations with explicit middleware |
nuxt-actions |
Broader action toolkit with query/cache, streaming, uploads, OpenAPI, and other tooling | Applications wanting those built-in abstractions |
Both action libraries offer schema validation, typed middleware, output validation, and optimistic updates. This module concentrates on the form and mutation path and composes with Nuxt's existing query tools. No speed, bundle-size, or security superiority is implied. See the comparison guide.
API essentials
const action = createSafeActionClient()
.use(async ({ ctx, next }) => next({ ctx: { ...ctx, requestLabel: 'example' } }))
.metadata({ name: 'greet' })
.schema(z.object({ name: z.string() }))
.outputSchema(z.object({ greeting: z.string() }))
.action(({ parsedInput, ctx, event }) => ({ greeting: `Hello, ${parsedInput.name}` }))
next({ ctx }) replaces the previous context. Spread ...ctx to preserve earlier fields. Call next() exactly once on the successful path; throw to reject the request. Middleware receives raw input; schema validation happens before the handler.
| Client method | Behavior |
|---|---|
execute(input) |
Starts execution; returns void |
executeAsync(input) |
Resolves { data?, businessError?, serverError?, validationErrors? }; does not unwrap data |
cancel() |
Invalidates pending updates, aborts owned requests, returns status to idle |
reset() |
Cancels pending execution and clears result state |
Only the latest execution updates reactive state and runs completion callbacks. Every execution promise still resolves its own result. Callback exceptions are logged and do not turn a successful result into a failure. Cancellation cannot undo a mutation that already reached the server. Automatic transport retries are disabled.
const { executeAsync, cancel } = useAction(createPost, {
timeout: 10_000,
cancelPrevious: true,
abortOnDispose: true, // opt-in; false by default
headers: () => ({ 'X-Request-Source': 'editor' }),
credentials: 'same-origin',
onSuccess({ data, input }) { console.log(data.id) },
onError({ error, input }) { console.error(error) },
onSettled({ result, input }) { /* latest completion only */ },
})
detached: true selects native browser fetch. Both transports can continue across SPA navigation unless explicitly cancelled; neither guarantees completion across page unload. Full client reference.
Recipes
- HTML forms and Nuxt UI nested field errors
- Authentication, ownership, and output allowlists
- Optimistic updates and rollback
- GET actions with Nuxt
useAsyncData - Validator choice, transforms, and async refinements
Configuration
export default defineNuxtConfig({
modules: ['nuxt-safe-action'],
safeAction: {
actionsDir: 'actions', // relative to serverDir
csrf: 'strict', // non-GET actions require same-origin JSON; 'off' to opt out
},
})
Development
pnpm install
pnpm dev:prepare
pnpm dev
pnpm verify
The verification command runs lint, types, tests, build, packed-install smoke validation, and documentation build. CI covers the Nuxt 3.8.0 floor plus the latest Nuxt 3/4 with Zod 3/4; validator tests also cover Valibot, ArkType, and Yup. See contributing and security reporting.
Inspiration and license
Inspired by next-safe-action, adapted for Nuxt. MIT.
Related Packages
Type-safe server actions for Nuxt with Standard Schema validation (Zod, Valibot,...
Runtime-validated contracts, status-typed clients, and OpenAPI for Nuxt server r...
Auto-generate Nitro defineRouteMeta (OpenAPI) for Nuxt server routes from JSDoc...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.8.1 | ^3.0 | MIT | |
| 0.7.0 | ^3.0 | MIT | |
| 0.6.0 | ^3.0 | MIT | |
| 0.5.0 | ^3.0 | MIT | |
| 0.4.1 | ^3.0 | MIT | |
| 0.4.0 | ^3.0 | MIT | |
| 0.3.0 | ^3.0 | MIT | |
| 0.2.1 | ^3.0 | MIT | |
| 0.2.0 | ^3.0 | MIT | |
| 0.1.1 | ^3.0 | MIT |