Simple Type-Safe Actions
Static type and runtime validation for server actions in
NextJS App Router
with Zod
Initialize your actions
import { prisma } from "your-prisma-instance"
import { getSession } from "your-session-lib"
import { CreateAction, ActionError } from "safe-action"
const meta = {
event: 'event-test',
channel: 'channel-test'
}
const context = async () => {
const session = getSession()
return {
prisma,
session
}
}
const action = CreateAction.meta(meta).context(context).create({
errorHandler: (error) => {
console.error(error)
}
})
export const publicAction = action
export const authedAction = action.middleware(async ({ ctx, next }) => {
if (!ctx.session) {
throw new ActionError({
code: "UNAUTHORIZED",
message: "You must be logged in to perform this action"
})
}
return next({
ctx: {
session: ctx.session
}
})
})
Create a server action using an Input Parser for parameter validation
[!TIP]
Use the .input() methods to validade the server actions parameters
[!IMPORTANT]
Parser methods only accepts ZodObject so use z.object()
You can chain methods to create more complex objects
Ex.: .input(z.object({ name: z.string() })).input(z.object({ age: z.number() }))
"use server"
import { z } from "zod"
import { authedAction } from "src/server/root.ts"
export const myAction = authedAction
.input(z.object({ name: z.string() }))
.input(z.object({ age: z.number() }))
.execute(async ({ input, ctx }) => {
return {
message: `${input.name} ${input.age}`,
}
})
Using an Output Parser for return validation
[!TIP]
Use the .output() methods to validate the server action return
[!IMPORTANT]
Parser methods only accept ZodObject so use z.object()
You can chain methods to create more complex objects in combination with input parsers
Ex.: .output(z.object({ name: z.string() })).output(z.object({ age: z.number() }))
"use server"
import { z } from "zod"
import { authedAction } from "src/server/root.ts"
export const myAction = authedAction
.input(z.object({ name: z.string() }))
.input(z.object({ age: z.number() }))
.output(z.object({ name: z.string() }))
.output(z.object({ age: z.number() }))
.execute(async ({ input, ctx }) => {
return {
age: input.age,
name: input.name
}
})
Adding middlewares to an action
[!TIP]
Use the .middleware() methods to add middlewares to an action
[!IMPORTANT]
Middlewares need to return the next() function to proceed to the next one
You can chain middlewares to create more complex logic
Middlewares have access to input, meta, rawInput (unvalidated input), as well as ctx and the next function to proceed with the stack
Middlewares can be either asynchronous or regular functions
Ex.: .middleware(async ({ input, rawInput, ctx, next }) => {...})
"use server"
import { z } from "zod"
import { authedAction } from "src/server/root.ts"
export const myAction = authedAction.middleware(async (opts) => {
const { meta, input, rawInput, ctx, next } = opts
return next()
}).middleware(({ next }) => {
return next({ ctx: { userId: 1 } })
})
Adding hooks to an action
[!TIP]
Use the .hook() methods to add hooks to an action
[!IMPORTANT]
Hooks run in three different life cycles and have access to values based on their life cycle
- onSuccess -
ctx | meta | rawInput | input
- onError -
ctx | meta rawInput | error
- onSettled
ctx | meta | rawInput
You can chain hooks of the same life cycle to create more complex logic
Hooks can be either asynchronous or regular functions
Ex.: .hook('onSuccess', async ({ ctx, meta, input, rawInput }) => {...})
"use server"
import { z } from "zod"
import { authedAction } from "src/server/root.ts"
export const myAction = authedAction.hook("onSuccess", async (opts) => {
const { ctx, meta, input, rawInput } = opts
await logger(`User has logged in with data: ${input}`)
}).hook("onSuccess", ({ rawInput }) => {
console.log(`Input without validation: ${rawInput}`)
}).hook("onError", async ({ rawInput, error }) => {
await logger(`User failed to login ${error.message}`)
})
Executing an action in a server component
import { myAction } from "src/server/user"
export default async function Page() {
const { data, error } = await myAction({ name: "John doe", age: 30 })
return (
<div>
{/* ⚠️ Always check to access the data and get inferred types */}
{data ? (
<>
<h1>{data.name}</h1>
<p>{data.age}</p>
</>
) : (
<div>{error.message}</div>
)}
</div>
)
}
Executing an action in a client component
[!TIP]
To use it in a client component, we will create a custom hook
import React from "react"
import { myAction } from "src/server/user"
import { type ActionInput } from "safe-action"
import { toast } from "sonner"
type Data = ActionInput<typeof myAction>
export const useCustomHook = () => {
const [isPending, startTransition] = React.useTransition()
const randomName = ({ name, age }: Data) => {
startTransition(async () => {
const { data, error } = await myAction({ name, age })
if (error) {
toast("Something went wrong", {
description: error.message
})
return
}
toast("Action executed successfully", {
description: `Data received ${data.name} ${data.age}`
})
})
}
return { isPending, randomName }
}