
Security News
White House Authorizes Private Companies to Conduct Offensive Cyber Operations
A new federal program will let vetted U.S. cybersecurity firms help investigate and disrupt foreign cybercrime groups under government direction.
safe-action
Advanced tools
Static type and runtime validation for server actions in NextJS App Router with Zod
// src/server/root.ts
import { prisma } from "your-prisma-instance"
import { getSession } from "your-session-lib"
import { CreateAction, ActionError } from "safe-action"
// You can add metadata that will be shared between middlewares and hooks
// Metadata must be an object
// You can always modify the values, but the types will always remain the same from when it was initialized
// ⚠️ If you do not initialize metadata, it will start as undefined: unknown and will remain unknown throughout the action
const meta = {
event: 'event-test',
channel: 'channel-test'
}
// You can initialize the action context
// Context must be a function with these signatures: () => object | () => Promise<object>
// ⚠️ If you do not provide the initial context, it will start as undefined: unknown
const context = async () => {
const session = getSession()
return {
prisma,
session
}
}
// ✅ Meta and context types will be inferred based on usage
const action = CreateAction.meta(meta).context(context).create({
// ✅ All errors thrown within actions will be handled here as well
errorHandler: (error) => {
// ⚠️ The error object is serialized to return from the server to the client
console.error(error)
}
})
export const publicAction = action
export const authedAction = action.middleware(async ({ ctx, next }) => {
if (!ctx.session) { // ⚠️ Ensure this action has a session
throw new ActionError({
code: "UNAUTHORIZED",
message: "You must be logged in to perform this action"
})
}
// ⚠️ It is important to use the next() function to call the next middleware in the stack
return next({
ctx: {
session: ctx.session // ✅ Pass the context forward, inferring the session
}
})
})
[!TIP] Use the
.input()methods to validade the server actions parameters
[!IMPORTANT] Parser methods only accepts
ZodObjectso usez.object()You can chain methods to create more complex objects
Ex.:
.input(z.object({ name: z.string() })).input(z.object({ age: z.number() }))
// src/server/user/index.ts
"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() }))
// input will have its type inferred based on the parser methods
// ✅ input: { name: string; age: number }
// ✅ ctx: { session: Session }
.execute(async ({ input, ctx }) => {
// do something with the data
// ✅ return inferred automatically
return {
message: `${input.name} ${input.age}`,
}
})
[!TIP] Use the
.output()methods to validate the server action return
[!IMPORTANT] Parser methods only accept
ZodObjectso usez.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() }))
// src/server/user/index.ts
"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() }))
// input will have its type inferred based on the parser methods
// ✅ input: { name: string; age: number }
// ✅ ctx: { session: Session }
.execute(async ({ input, ctx }) => {
// do something with the data
// ✅ return inferred based on output parsers
return {
age: input.age,
name: input.name
}
})
[!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 asctxand thenextfunction to proceed with the stackMiddlewares can be either asynchronous or regular functions
Ex.:
.middleware(async ({ input, rawInput, ctx, next }) => {...})
// src/server/user/index.ts
"use server"
import { z } from "zod"
import { authedAction } from "src/server/root.ts"
// ⚠️ for security, rawInput will always have type: unknown because it is not validated
export const myAction = authedAction.middleware(async (opts) => {
const { meta, input, rawInput, ctx, next } = opts
// ⚠️ return the next() function to proceed with the middleware stack
return next()
}).middleware(({ next }) => {
// ✅ you can add new properties to the context object
return next({ ctx: { userId: 1 } }) // ✅ ctx: { session: Session, userId: number }
})
[!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|metarawInput|error- onSettled
ctx|meta|rawInputYou 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 }) => {...})
// src/server/user/index.ts
"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
// ✅ E.g. You can use hooks to monitor and use logs
await logger(`User with 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}`)
})
// src/app/page.tsx
import { myAction } from "src/server/user"
export default async function Page() {
// ✅ Parameters typed according to input parsers
const { data, error } = await myAction({ name: "John doe", age: 30 })
return (
<div>
{/* ⚠️ Always check to access the data */}
{data ? (
<>
<h1>{data.name}</h1>
<p>{data.age}</p>
</>
) : (
<div>{error.message}</div>
)}
</div>
)
}
[!TIP] To use it in a client component, we will create a custom hook
// src/hooks/index.ts
import React from "react"
import { myAction } from "src/server/user"
// type helper to help us get the parameters of an action
import { type ActionInput } from "safe-action"
// Let's use the shadcn/ui toast component as an example
import { toast } from "sonner"
// Let's create a type for the values we will need to receive in the action
type Data = ActionInput<typeof myAction> // ✅ Data = { name: string; age: number }
export const useCustomHook = () => {
const [isPending, startTransition] = React.useTransition()
const randomName = ({ name, age }: Data) => {
startTransition(async () => {
const { data, error } = await myAction({ name, age })
if (error) {
// ✅ You can show an alert or toast to the user
toast("Something went wrong", {
description: error.message
})
// ⚠️ return to stop the flow so the success result will be inferred
return
}
toast("Action executed successfully", {
description: `Data received ${data.name} ${data.age}`
})
})
}
return { isPending, randomName }
}
FAQs
Simple type-safe actions
The npm package safe-action receives a total of 70 weekly downloads. As such, safe-action popularity was classified as not popular.
We found that safe-action demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
A new federal program will let vetted U.S. cybersecurity firms help investigate and disrupt foreign cybercrime groups under government direction.

Research
/Security News
The campaign amassed more than 75,000 installs by targeting Russian-speaking users seeking access to blocked services.

Company News
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.