npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

safe-action

v0.8.2

Published

Simple type-safe actions

Downloads

123

Readme

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