Sign In

safe-action

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install
Package version was removed
This package version has been unpublished, mostly likely due to security reasons
This package has malicious versions linked to the ongoing "Mini Shai-Hulud" supply chain attack.

Affected versions:

0.8.30.8.4
View campaign page

safe-action

Simple type-safe actions

unpublished
Source
npmnpm
Version
0.8.3
Version published
Weekly downloads
77
92.5%
Maintainers
1
Weekly downloads
 
Created
Source

Simple Type-Safe Actions

Static type and runtime validation for server actions in NextJS App Router with Zod

Initialize your actions

// 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"
    })
  }

	// ⚠️ If you want to pass new context forward or modify, you must use the next() function
  return next({
    ctx: {
      session: ctx.session // ✅ Pass the context forward, inferring the 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() }))

// 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}`,
    }
  })

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() }))

// 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
    }
  })

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 }) => {...})

// 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 }
})

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 }) => {...})

// 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 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

// 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 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

// 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 }
}

Keywords

action

FAQs

Package last updated on 11 May 2026

Did you know?

Socket

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.

Install

Related posts