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-fn-react

v0.3.3

Published

SafeFn is a library to easily build **fully** typed functions with validated input and output, procedure chaining, callbacks and more. This is ideal for functions that are at the edge of your applications, like Server Actions!

Downloads

27

Readme

SafeFn

SafeFn is a library to easily build fully typed functions with validated input and output, procedure chaining, callbacks and more. This is ideal for functions that are at the edge of your applications, like Server Actions!

Why

Existing solutions like ZSA and Next-Safe-Action are great, but often fall short when it comes to handling errors. SafeFn integrated NeverThrow to enable typed errors (check out Matt Pocock's Twitter thread for a quick intro if you're not familiar).

While it's possible to almost completely ignore Neverthrow to incrementally adopt it as the only requirement is that your handler functions return a Result, this library really shines when used in conjunction with it. safeHandler() builds on Neverthrow's safeTry() and allows for ergonomic "return early if error" handling, similar to how throwing would work but with full type safety!.

It also provides a React library to easily call these server actions while taking care of transitions, pending states etc.

Personal project and still WIP.

Documentation

The docs can be found here.

Quick example

Take the simple case of a function that creates a todo for a user (very original I know). We first need to make sure the user is signed in, then validate the input of the todo, store it in the database if it's valid and then return it.

All of this needs to be done while taking care of errors that might occur in any of these steps.

const withAuth = createSafeFn().handler(async () => {
  const user = await auth.getSignedInUser();
  if (!user) {
    return err({
      code: "NOT_AUTHORIZED",
    });
  }
  return ok(user);
});
const createTodo = createSafeFn()
  .use(withAuth)
  .input(
    z.object({
      title: z.string().min(2),
      description: z.string().min(2),
    }),
  )
  .handler(async (args) => {
    const user = args.ctx;
    const todo = await db.todo.create({
      title: args.input.title,
      description: args.input.description,
      userId: user.id,
    });
    return ok(todo);
  });

Considering both of these functions can throw we should probably add some error handling as such:

const withAuth = SafeFn.new()
  ...
  .catch((e) => {
    return err({
      code: "AUTH_ERROR",
    });
  });

const createTodo = SafeFn.new(withAuth)
  ...
  .catch((e) => {
    return err({
      code: "DB_ERROR",
    });
  });

This results in a fully typed function with the following return type:

type res = ResultAsync<
  {
    id: string;
    title: string;
    description: string;
  },
  | {
      code: "DB_ERROR";
    }
  | { code: "AUTH_ERROR" }
  | { code: "NOT_AUTHORIZED" }
  | {
      code: "INPUT_PARSING";
      cause: {
        formattedError: z.ZodFormattedError<{
          title: string;
          description: string;
        }>;
        flattenedError: z.ZodFlattenedError<{
          title: string;
          description: string;
        }>;
      };
    }
>;

we can easily call this function using the run() method:

const res = await createTodo({...})

if (res.isOk()){
  ...
} else if (res.isErr()){
  if (res.error.code === "AUTH_ERROR"){
    ...
  }
}

or on the client by creating an action and calling it with useServerAction(), in this example we're adding a platform specific callback to handle revalidation:

"use server";

const createTodoAction = createTodo()
  .onSuccess(async (args) => {
    revalidatePath(`/todos/${args.value.id}`);
  })
  .createAction();
const { execute, result, isPending } = useServerAction(createTodoAction);

However, the magic of the integration of NeverThrow really comes out if your other functions are also returning a Result or ResultAsync. Instead of passing a regular function via handler() you can also use safeHandler(). This builds on Neverthrow's safeTry() and takes in a generator function. This generator function receives the same args as handler(), but it allows you to yield * other results using .safeUnwrap(). This is meant to emulate Rust's ? operator and allows a very ergonomic way to write "return early if this fails, otherwise continue" logic.

This function has the same return type as the handler() example above.

const withAuth = createSafeFn().safeHandler(async function* () {
  const user = yield* auth.getSignedInUser().safeUnwrap();
  return ok(user);
});

const createTodoAction = createSafeFn()
  .use(withAuth)
  .input(
    z.object({
      title: z.string().min(2),
      description: z.string().min(2),
    }),
  )
  .safeHandler(async function* (args) {
    const user = args.ctx;
    const todo = yield* db.todo
      .create({
        title: args.input.title,
        description: args.input.description,
        userId: user.id,
      })
      .safeUnwrap();
    return ok(todo);
  });