Guide

Action Client

Create and configure the nuxt-safe-action client with custom error handling, authentication middleware, and role-based access control.

Action Client

The action client is the foundation for all your server actions. Create it once and reuse it across your application.

Basic Client

server/utils/action-client.ts
import { createSafeActionClient } from '#safe-action'

export const actionClient = createSafeActionClient({
  handleServerError: (error) => {
    console.error('Action error:', error.message)
    return error.message
  },
})

The handleServerError callback transforms server errors before they're sent to the client. This is where you can log errors, sanitize messages, or integrate with error tracking services.

Authenticated Client

You can extend the base client with middleware to create specialized clients:

server/utils/action-client.ts
import { createSafeActionClient } from '#safe-action'

export const actionClient = createSafeActionClient({
  handleServerError: (error) => {
    console.error('Action error:', error.message)
    return error.message
  },
})

// Authenticated client - requires a valid session
export const authActionClient = actionClient
  .use(async ({ next, event }) => {
    const session = await getUserSession(event)
    if (!session) throw new Error('Unauthorized')
    return next({ ctx: { userId: session.user.id } })
  })

Now any action built with authActionClient will automatically require authentication, and the handler will have access to ctx.userId:

server/actions/update-profile.ts
import { z } from 'zod'
import { authActionClient } from '../utils/action-client'

export default authActionClient
  .schema(z.object({
    displayName: z.string().min(1).max(50),
  }))
  .action(async ({ parsedInput, ctx }) => {
    // ctx.userId is available and typed!
    await db.user.update({
      where: { id: ctx.userId },
      data: { displayName: parsedInput.displayName },
    })
    return { success: true }
  })

Multiple Clients

You can create as many specialized clients as you need:

server/utils/action-client.ts
// Base client
export const actionClient = createSafeActionClient({ ... })

// Requires authentication
export const authActionClient = actionClient.use(async ({ next, event }) => { ... })

// Requires admin role
export const adminActionClient = authActionClient.use(async ({ next, ctx }) => {
  const user = await db.user.findUnique({ where: { id: ctx.userId } })
  if (user?.role !== 'admin') throw new Error('Forbidden')
  return next({ ctx: { ...ctx, isAdmin: true } })
})

Each client inherits the middleware chain from its parent, so adminActionClient runs both the auth check and the admin check.

Copyright © 2026