nuxt-actions
nuxt-actions

Type-safe server actions with Standard Schema validation, middleware, optimistic updates, streaming, and SSR queries

nuxt-actions logo

nuxt-actions

npm version npm downloads License Nuxt codecov

Documentation | Playground | Example

Type-safe server actions for Nuxt with Standard Schema validation, middleware, builder pattern, and optimistic updates.

Works with Zod, Valibot, ArkType, and any Standard Schema compliant library.


Features

  • Standard Schema - Use Zod, Valibot, ArkType, or any compliant validation library
  • E2E Type Inference - Import typed action references from #actions with zero manual generics
  • Builder Pattern - createActionClient() for composing actions with shared middleware
  • Optimistic Updates - useOptimisticAction with race-safe rollback
  • SSR Queries - useActionQuery wraps useAsyncData for SSR, caching, and reactive re-fetching
  • Streaming Actions - defineStreamAction + useStreamAction for real-time AI/streaming use cases
  • Retry/Backoff - Native ofetch retry with retry: true | number | { count, delay, statusCodes }
  • Request Deduplication - dedupe: 'cancel' | 'defer' to prevent duplicate requests
  • Custom Headers - Per-request auth tokens via static headers or function
  • HMR Type Updates - Action file changes update types without restarting dev server
  • DevTools Tab - Nuxt DevTools integration showing registered actions
  • Security Hardened - Prototype pollution protection, error message sanitization, double next() prevention
  • Output Validation - Validate server responses, not just inputs
  • Middleware Chain - Reusable, composable middleware with typed context accumulation
  • Type Tests - 24 compile-time type tests verifying type inference correctness
  • Zero Config - Auto-imported, works out of the box

Quick Setup

Install the module:

npx nuxi module add nuxt-actions

Then install your preferred validation library:

# Zod (most popular)
pnpm add zod

# Valibot (smallest bundle)
pnpm add valibot

# ArkType (fastest runtime)
pnpm add arktype

That's it! All utilities are auto-imported.

Usage

Simple Mode: defineAction

Create type-safe API routes with automatic input validation:

// server/api/todos.post.ts
import { z } from 'zod'

export default defineAction({
  input: z.object({
    title: z.string().min(1, 'Title is required'),
  }),
  handler: async ({ input }) => {
    const todo = await db.todo.create({ data: input })
    return todo
  },
})

Works with any Standard Schema library:

// With Valibot
import * as v from 'valibot'

export default defineAction({
  input: v.object({ title: v.pipe(v.string(), v.minLength(1)) }),
  handler: async ({ input }) => ({ id: Date.now(), title: input.title }),
})
// With ArkType
import { type } from 'arktype'

export default defineAction({
  input: type({ title: 'string > 0' }),
  handler: async ({ input }) => ({ id: Date.now(), title: input.title }),
})

Builder Mode: createActionClient

Share middleware, metadata, and configuration across actions:

// server/utils/action-clients.ts
export const authClient = createActionClient()
  .use(authMiddleware)
  .use(rateLimitMiddleware)

export const adminClient = createActionClient()
  .use(authMiddleware)
  .use(adminMiddleware)
// server/api/admin/users.get.ts
import { z } from 'zod'
import { adminClient } from '~/server/utils/action-clients'

export default adminClient
  .schema(z.object({
    page: z.coerce.number().default(1),
  }))
  .metadata({ role: 'admin', action: 'list-users' })
  .action(async ({ input, ctx }) => {
    // ctx.user and ctx.isAdmin available from middleware chain
    return await db.user.findMany({
      skip: (input.page - 1) * 10,
      take: 10,
    })
  })

Output Schema Validation

Validate what your server returns, not just what it receives:

export default defineAction({
  input: z.object({ id: z.string() }),
  outputSchema: z.object({
    id: z.string(),
    name: z.string(),
    email: z.string().email(),
  }),
  handler: async ({ input }) => {
    return await db.user.findUnique({ where: { id: input.id } })
  },
})

Client: useAction

Call server actions from Vue components with reactive state:

<script setup lang="ts">
const { execute, executeAsync, data, error, status, reset } = useAction<
  { title: string },
  { id: number; title: string }
>('/api/todos', {
  method: 'POST',
  onExecute(input) {
    console.log('Sending:', input)
  },
  onSuccess(data) {
    toast.success(`Created: ${data.title}`)
  },
  onError(error) {
    toast.error(error.message)
  },
})

// Option 1: Full result with success/error
async function handleSubmit(title: string) {
  const result = await execute({ title })
  if (result.success) console.log(result.data)
}

// Option 2: Direct data (throws on error)
async function handleSubmitAsync(title: string) {
  try {
    const todo = await executeAsync({ title })
    console.log(todo)
  } catch (err) {
    // err is ActionError
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit('Buy milk')">
    <button :disabled="status === 'executing'">
      {{ status === 'executing' ? 'Creating...' : 'Add Todo' }}
    </button>
    <p v-if="error" class="error">{{ error.message }}</p>
  </form>
</template>

Optimistic Updates: useOptimisticAction

Instant UI updates with automatic rollback on server error:

<script setup lang="ts">
const todos = ref([
  { id: 1, title: 'Buy milk', done: false },
  { id: 2, title: 'Walk dog', done: true },
])

const { execute, optimisticData } = useOptimisticAction('/api/todos/toggle', {
  method: 'PATCH',
  currentData: todos,
  updateFn: (input, current) =>
    current.map(t => t.id === input.id ? { ...t, done: !t.done } : t),
  onError(error) {
    toast.error('Failed to update - changes reverted')
  },
})
</script>

<template>
  <ul>
    <li v-for="todo in optimisticData" :key="todo.id">
      <input
        type="checkbox"
        :checked="todo.done"
        @change="execute({ id: todo.id })"
      >
      {{ todo.title }}
    </li>
  </ul>
</template>

Middleware

Create reusable middleware for cross-cutting concerns:

// server/utils/auth.ts
export const authMiddleware = defineMiddleware(async ({ event, next }) => {
  const session = await getUserSession(event)
  if (!session) {
    throw createActionError({
      code: 'UNAUTHORIZED',
      message: 'Authentication required',
      statusCode: 401,
    })
  }
  return next({ ctx: { user: session.user } })
})

Publish standalone middleware as npm packages:

// Published as `nuxt-actions-ratelimit`
export const rateLimitMiddleware = createMiddleware(async ({ event, next }) => {
  await checkRateLimit(event)
  return next()
})

Error Handling

Throw typed errors from handlers or middleware:

throw createActionError({
  code: 'NOT_FOUND',
  message: 'Todo not found',
  statusCode: 404,
})

// With field-level errors
throw createActionError({
  code: 'VALIDATION_ERROR',
  message: 'Duplicate entry',
  statusCode: 422,
  fieldErrors: {
    email: ['Email is already taken'],
  },
})

All errors follow a consistent format:

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Input validation failed",
    "statusCode": 422,
    "fieldErrors": {
      "title": ["Title is required"],
      "email": ["Invalid email address"]
    }
  }
}

API Reference

Server Utilities

defineAction(options)

OptionTypeDescription
inputStandardSchemaAny Standard Schema compliant schema for input validation
outputSchemaStandardSchemaSchema for output validation
middlewareActionMiddleware[]Array of middleware functions
metadataRecord<string, unknown>Metadata for logging/analytics
handler(params) => Promise<T>Handler receiving { input, event, ctx }

createActionClient(options?)

MethodDescription
.use(middleware)Add middleware to the chain
.schema(inputSchema)Set input validation schema
.metadata(meta)Attach metadata
After .schema():
.outputSchema(schema)Set output validation schema
.metadata(meta)Attach metadata
.action(handler)Terminal - creates the event handler

defineMiddleware(fn) / createMiddleware(fn)

Define a typed middleware function. createMiddleware is an alias that signals intent for publishable middleware.

createActionError(options)

OptionTypeDefaultDescription
codestringrequiredError code identifier
messagestringrequiredHuman-readable message
statusCodenumber400HTTP status code
fieldErrorsRecord<string, string[]>-Field-level errors

Client Composables

useAction<TInput, TOutput>(path, options?)

OptionTypeDefaultDescription
methodHttpMethod'POST'HTTP method
headersRecord<string, string> | () => Record-Static or dynamic headers
retryboolean | number | RetryConfigfalseRetry configuration
dedupe'cancel' | 'defer'-Request deduplication
onExecute(input) => void-Called before fetch
onSuccess(data) => void-Success callback
onError(error) => void-Error callback
onSettled(result) => void-Settled callback

Returns: { execute, executeAsync, data, error, status, isIdle, isExecuting, hasSucceeded, hasErrored, reset }

useOptimisticAction<TInput, TOutput>(path, options)

OptionTypeDescription
methodHttpMethodHTTP method (default: 'POST')
headersRecord<string, string> | () => RecordStatic or dynamic headers
retryboolean | number | RetryConfigRetry configuration
currentDataRef<TOutput>Source of truth data ref
updateFn(input, current) => TOutputOptimistic update function

Returns: { execute, optimisticData, data, error, status, isIdle, isExecuting, hasSucceeded, hasErrored, reset }

useActionQuery(action, input?, options?)

SSR-capable GET action query wrapping useAsyncData:

OptionTypeDefaultDescription
serverbooleantrueRun on SSR
lazybooleanfalseDon't block navigation
immediatebooleantrueExecute immediately
default() => T-Default value factory

Returns: { data, error, status, pending, refresh, clear }

useStreamAction(action, options?)

Client composable for streaming server actions:

OptionTypeDescription
onChunk(chunk) => voidCalled for each chunk
onDone(allChunks) => voidCalled when stream completes
onError(error) => voidCalled on error

Returns: { execute, stop, chunks, data, status, error }

defineStreamAction(options)

Server-side streaming action with SSE:

OptionTypeDescription
inputStandardSchemaInput validation schema
middlewareActionMiddleware[]Middleware chain
handler({ input, event, ctx, stream }) => voidStreaming handler

Why nuxt-actions?

Featurenuxt-actionstrpc-nuxtnext-safe-action
FrameworkNuxtNuxtNext.js
Standard Schema (Zod + Valibot + ArkType)Zod onlyZod / Yup / Valibot
E2E type inference
Builder pattern
Middleware with typed context
Optimistic updates composable
SSR queries
Streaming actions (SSE)
Retry / backoff
Request deduplication
Output schema validation
DevTools integration
HMR type updates
Security hardening (6 layers)
Zero config
Nuxt-native (no protocol layer)

Sponsors

If you find this module useful, consider supporting the project:

Sponsor

Contribution

Local development
# Install dependencies
pnpm install

# Generate type stubs
pnpm run dev:prepare

# Develop with the playground
pnpm run dev

# Run ESLint
pnpm run lint

# Run Vitest
pnpm run test
pnpm run test:watch

# Build the module
pnpm run prepack

License

MIT