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

next-action

v0.2.0

Published

Provides a mechanism for validate and execute server actions

Downloads

11

Readme

next-action

CI npm version Bundle Size docs

Provides a centralized way to call your server actions.

Installation

npm install next-action
yarn add next-action
pnpm add next-action
bun add next-action

API Docs

https://neo-ciber94.github.io/next-action

Why?

Server actions are great but have some caveats on NextJS:

  • Cannot be intercepted by middlewares
  • Cannot throw errors

And as any other API endpoint the user input needs to be validated.

next-action provide an API to easily validate, throw errors and add middlewares to your server actions.

Table of contents

Usage

// lib/action.ts
import { createServerActionProvider } "next-action/server";

export const publicAction = createServerActionProvider();
// lib/actions/api.ts
"use server";

// Any object that have a `parse` method can be used as validator
const schema = z.object({
  title: z.string(),
  content: z.string(),
});

export const createPost = publicAction(
  schema,
  async ({ input }) => {
    const postId = crypto.randomUUID();
    await db.insert(posts).values({ postId, ...input });
    return { postId };
  });

You can call the createPost directly client and server side as any other server action. Client side you can also use useAction or useFormAction which allow you to track the loading, error and success state of the server action.

// app/create-post/page.tsx
"use client";

import { useAction } from "next-action/react";

export default function CreatePostPage() {
  const { 
      execute, 
      data, 
      error, 
      status, 
      isExecuting, 
      isError, 
      isSuccess 
    } = useAction(createPost, {
      onSuccess(data) {
        // success
      },
      onError(error) {
        // error
      },
      onSettled(result) {
        // completed
      },
    }
  );

  return <>{/* Create post form */}</>;
}

Using form actions

You can also define and call server actions that accept a form, you define the actions using formAction on your action provider.

'use server';

const schema = z.object({
  postId: z.string()
  title: z.string(),
  content: z.string(),
});

export const updatePost = publicAction.formAction(
  schema,
  async ({ input }) => {
    await db.update(posts)
      .values({ postId, ...input })
      .where(eq(input.postId, posts.id))

    return { postId };
  });

updatePost will have the form: (input: FormData) => ActionResult<T>, so you can use it in any form.

// app/update-post/page.tsx
"use client";

export default function UpdatePostPage() {
  return (
    <form action={updatePost}>
      <input name="postId" />
      <input name="title" />
      <input name="content" />
    </form>
  );
}

To track the progress of a form action client side you use the useFormAction hook.

const { 
      action, 
      data, 
      error, 
      status, 
      isExecuting, 
      isError, 
      isSuccess 
    } = useFormAction(updatePost, {
    onSuccess(data) {
      // success
    },
    onError(error) {
      // error
    },
    onSettled(result) {
      // completed
    },
  }
);

Then you can use the returned action on your <form action={...}>.

Throwing errors

You can throw any error in your server actions, those errors will be send to the client on the result.

// lib/actions/api.ts
"use server";

import { ActionError } from "next-action";

export const deletePost = publicAction(async ({ input }) => {
  throw new ActionError("Failed to delete post");
});

We recommend using ActionError for errors you want the client to receive.

Map errors

For sending the errors to the client you need to map the error to other type, by default we map it to string, you map your errors in the createServerActionProvider.

import { defaultErrorMapper } from "next-action/utils";

export const publicAction = createServerActionProvider({
  mapError(err: any) {
    // You need to manage manually your validation errors
    if (err instanceof ZodError) {
      return err.issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n");
    }

    // Use the default mappinh to string
    return defaultErrorMapper(err);
  },
});

Context

You can also set a context that all your server actions will have access to.

// lib/action.ts
import { createServerActionProvider } "next-action/server";

export const action = createServerActionProvider({
  context() {
    return { db }
  }
});

The context will be created each time the server action is called, after that you can access the context values on your server actions.

// lib/actions/api.ts
const schema = z.object({ postId: z.string() });

export const deletePost = action(
  async ({ input, context }) => {
    return context.db.delete(posts).where(eq(input.postId, posts.id));
  },
  {
    validator: schema,
  },
);

Middlewares

You can run a middleware before and after running your server actions.

Before server action

import { createServerActionProvider } "next-action/server";

export const authAction = createServerActionProvider({
  async onBeforeExecute({ input, context  }) {
    const session = await getSession();

    if (!session) {
      throw new ActionError("Unauthorized")
    }

    return { ...context, session }
  }
});

You can access the new context on all your actions.

// lib/actions/api.ts
const schema = z.object({
  postId: z.string(),
  title: z.string(),
  content: z.string(),
});

export const createPost = authAction(async ({ input, context }) => {
  await db.insert(users).values({ ...input, userId: context.session.userId });
}, {
  validator:
})

After server action

import { createServerActionProvider } "next-action/server";

export const authAction = createServerActionProvider({
  onBeforeExecute({ input }) {
    return { startTime: Date.now() }
  },
  onAfterExecute({ context }) {
    const elapsed = Date.now() - context.startTime;
    console.log(`Server action took ${elapsed}ms`);
  }
});

Testing Server Actions

Currently for test server actions is necessary to expose them as API endpoints, we serialize and deserialize the values in a similar way react does to ensure the same behavior.

// api/testactions/[[...testaction]]/route.ts
import { exposeServerActions } from "next-action/testing/server";

const handler = exposeServerActions({ actions: { createPost } });
export type TestActions = typeof handler.actions;
export const POST = handler;

You should set the EXPOSE_SERVER_ACTIONS environment variable to expose the endpoints.

And on your testing side

import { createServerActionClient } from "next-action/testing/client";

beforeAll(() => {
  // Start your nextjs server
});

test("Should create post", async () => {
  const client = createServerActionClient<TestActions>("http://localhost:3000/api/testactions");
  const res = await client.createPost({ title: "Post 1", content: "This is my first post" });
  const result = await res.json();
  expect(result.success).toBeTruthy();
});

See also these libraries that inspired next-action

  • https://github.com/TheEdoRan/next-safe-action
  • https://github.com/trpc/trpc