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.