Skip to content

Why nuxt-actions?

The Problem

Nuxt provides excellent primitives for fetching data -- useFetch, useAsyncData, $fetch -- but offers no built-in pattern for mutations and actions. When you need to create a todo, update a user profile, or process a payment, you are on your own.

This leads to repetitive, error-prone code across every server route:

Manual validation in every route

ts
// server/api/todos.post.ts -- without nuxt-actions
export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  // Manual validation -- repeated in every route
  if (!body.title || typeof body.title !== 'string') {
    throw createError({ statusCode: 422, message: 'Title is required' })
  }
  if (body.title.length > 200) {
    throw createError({ statusCode: 422, message: 'Title too long' })
  }

  const todo = await db.todo.create({ data: { title: body.title } })
  return todo
})

No type safety for inputs

The body variable above is unknown. You get no autocomplete, no compile-time checks, and no guarantee that the data matches what you expect. Bugs hide until runtime.

No middleware pattern

Authentication, rate limiting, and logging must be duplicated or wired up manually in every route. There is no composable way to say "this action requires auth" and have the context flow through.

Inconsistent error handling

Some routes return { error: "..." }, others throw createError, others return HTTP status codes with no body. The client has to guess the format.

No optimistic updates

Instant UI feedback for mutations requires manual snapshot/rollback logic in every component. Most teams skip it entirely.

The Solution

nuxt-actions solves all of these problems with a single, cohesive API:

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

export default defineAction({
  input: z.object({
    title: z.string().min(1).max(200),
  }),
  handler: async ({ input }) => {
    return await db.todo.create({ data: { title: input.title } })
  },
})
vue
<script setup lang="ts">
const { execute, data, error, status } = useAction<
  { title: string },
  { id: number; title: string }
>('/api/todos')

await execute({ title: 'Buy milk' })
</script>

That is the entire implementation -- server and client. You get validated input, typed handler parameters, a consistent error format, and reactive state management.

What You Get

  • Automatic input validation with field-level errors, powered by any Standard Schema library
  • Type-safe handlers where input is inferred from your schema
  • Middleware chains that accumulate typed context (auth, rate limiting, logging)
  • Builder pattern for sharing middleware across actions with createActionClient
  • Output validation to prevent data leaks and enforce API contracts
  • Optimistic updates with automatic rollback via useOptimisticAction
  • Consistent error format across all actions: { success, data } or { success, error }
  • Auto-imported utilities -- no manual imports for defineAction, useAction, or defineMiddleware

Comparison

Featurenuxt-actionstrpc-nuxtform-actions-nuxtnuxt-server-fn
Standard Schema (multi-library)❌ Zod only
Builder pattern
Middleware with typed context
Optimistic updates composable
SSR queries
Streaming actions (SSE)
Retry / backoff
Request deduplication⚠️ Via tanstack
Output schema validation
DevTools integration
HMR type updates
Security hardening (6 layers)
Auto-imported utilities
Zero config
Nuxt-native (no protocol layer)
Actively maintained❌ 30+ issues

See it in action

Explore the example repository for real-world usage with CRUD, streaming, optimistic updates, middleware, and more.

vs trpc-nuxt

trpc-nuxt (780+ stars, 6K weekly downloads) brings the full tRPC stack to Nuxt. It is the most established option in this space, but introduces a custom protocol layer, router definitions, and client setup that sit outside Nuxt's conventions.

nuxt-actions uses Nuxt's native file-based routing and $fetch. There is no router to define, no client to configure, and no protocol overhead. Actions are regular Nitro event handlers -- they work with curl, Postman, and any HTTP client without a special adapter.

tRPC also depends on Zod specifically for validation. nuxt-actions accepts any Standard Schema library, giving you the freedom to choose Valibot for smaller bundles or ArkType for runtime performance.

vs form-actions-nuxt

form-actions-nuxt (136 stars, listed on nuxt.com/modules) focuses on SvelteKit-inspired HTML form submissions with progressive enhancement. It targets a different use case -- form-centric interactions rather than general-purpose type-safe actions. The project has 30 open issues and has not been actively maintained.

vs nuxt-server-fn

nuxt-server-fn (282 stars) by Anthony Fu lets you call server functions from the client as if they were local. It is a clean, minimal idea but has not been updated since its initial release. There is no input validation, no middleware, no optimistic updates, and no error standardization.

When to Use nuxt-actions

nuxt-actions is designed for Nuxt applications that need structured server mutations. It is a good fit when:

  • You have more than a handful of POST/PUT/DELETE endpoints
  • You want validated, typed inputs without manual boilerplate
  • You need shared middleware for auth, rate limiting, or logging
  • You want consistent error responses across all actions
  • You want optimistic updates without building the rollback logic yourself

If your application only reads data and rarely mutates, the built-in useFetch and useAsyncData composables are likely sufficient. nuxt-actions complements them -- it handles the write side while Nuxt handles the read side.

Released under the MIT License.