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

nuxt-actions

v1.0.3

Published

Type-safe server actions for Nuxt with Standard Schema validation (Zod, Valibot, ArkType), middleware, builder pattern, optimistic updates, SSR queries, streaming, and retry/dedupe

Readme


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)

| Option | Type | Description | |--------|------|-------------| | input | StandardSchema | Any Standard Schema compliant schema for input validation | | outputSchema | StandardSchema | Schema for output validation | | middleware | ActionMiddleware[] | Array of middleware functions | | metadata | Record<string, unknown> | Metadata for logging/analytics | | handler | (params) => Promise<T> | Handler receiving { input, event, ctx } |

createActionClient(options?)

| Method | Description | |--------|-------------| | .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)

| Option | Type | Default | Description | |--------|------|---------|-------------| | code | string | required | Error code identifier | | message | string | required | Human-readable message | | statusCode | number | 400 | HTTP status code | | fieldErrors | Record<string, string[]> | - | Field-level errors |

Client Composables

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

| Option | Type | Default | Description | |--------|------|---------|-------------| | method | HttpMethod | 'POST' | HTTP method | | headers | Record<string, string> \| () => Record | - | Static or dynamic headers | | retry | boolean \| number \| RetryConfig | false | Retry 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)

| Option | Type | Description | |--------|------|-------------| | method | HttpMethod | HTTP method (default: 'POST') | | headers | Record<string, string> \| () => Record | Static or dynamic headers | | retry | boolean \| number \| RetryConfig | Retry configuration | | currentData | Ref<TOutput> | Source of truth data ref | | updateFn | (input, current) => TOutput | Optimistic update function |

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

useActionQuery(action, input?, options?)

SSR-capable GET action query wrapping useAsyncData:

| Option | Type | Default | Description | |--------|------|---------|-------------| | server | boolean | true | Run on SSR | | lazy | boolean | false | Don't block navigation | | immediate | boolean | true | Execute immediately | | default | () => T | - | Default value factory |

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

useStreamAction(action, options?)

Client composable for streaming server actions:

| Option | Type | Description | |--------|------|-------------| | onChunk | (chunk) => void | Called for each chunk | | onDone | (allChunks) => void | Called when stream completes | | onError | (error) => void | Called on error |

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

defineStreamAction(options)

Server-side streaming action with SSE:

| Option | Type | Description | |--------|------|-------------| | input | StandardSchema | Input validation schema | | middleware | ActionMiddleware[] | Middleware chain | | handler | ({ input, event, ctx, stream }) => void | Streaming handler |

Why nuxt-actions?

Sponsors

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

Contribution

# 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