Introduction

zsa is the best library for building typesafe server actions in NextJS. Built for a simple, scalable developer experience. Some majors features include...

  • Validated inputs/outputs with zod (hence the name Zod Server Actions)
  • Procedures (aka middleware) that pass context to your server actions
  • React Query integration for querying server actions in client components

and much more!

To get started, you can install zsa using any package manager:

npm i zsa zsa-react zod

Zod provides a simple way to define and validate types in your code.

Creating your first action

There's plenty of functionality with zsa, but to start, here is how you make a simple, validated server action:

(we know this function is silly but it gets the point across)

actions.ts
"use server"

import { createServerAction } from "zsa"
import z from "zod"

export const incrementNumberAction = createServerAction() 
    .input(z.object({
        number: z.number()
    }))
    .handler(async ({ input }) => {
        // Sleep for .5 seconds
        await new Promise((resolve) => setTimeout(resolve, 500))
        // Increment the input number by 1
        return input.number + 1;
    });

Let's break down the code:

  • createServerAction initializes a server action.
  • input sets the input schema for the action using a Zod schema.
  • handler sets the handler function for the action. The input is automatically validated based on the input schema.

A ZSAError with the code INPUT_PARSE_ERROR will be returned if the handler's input is does not match input schema.

Calling from the server

Server actions can also be called directly from the server without the need for a try/catch block.

example.ts
"use server"

const [data, err] = await incrementNumberAction({ number: 24 }); 

if (err) {
    return;
} else {
    console.log(data); // 25
}

The action will return either [data, null] on success or [null, err] on error.

Calling from the client

The most lightweight way to call your server action is to just call it! That is the beauty of server actions.

increment-example.tsx
"use client"

import { incrementNumberAction } from "./actions";
import { useState } from "react";
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from "ui";

export default function IncrementExample() {
    const [counter, setCounter] = useState(0);
    return (
        <Card>
            <CardHeader>
                <CardTitle>Increment Number</CardTitle>
            </CardHeader>
            <CardContent className="flex flex-col gap-4">
                <Button
                    onClick={async () => {
                        const [data, err] = await incrementNumberAction({ 
                            number: counter, 
                        }) 

                        if (err) {
                            // handle error
                            return
                        }

                        setCounter(data);
                    }}
                >
                    Invoke action
                </Button>
                <p>Count:</p>
            </CardContent>
        </Card>
    );
}

However, usually you will want to use the useServerAction hook to make your life easier.

Server actions come with built-in loading states, making it easy to handle asynchronous operations. Here's an example of using the incrementNumberAction as a mutation:

increment-example.tsx
"use client"

import { incrementNumberAction } from "./actions";
import { useServerAction } from "zsa-react";
import { useState } from "react";
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from "ui";

export default function IncrementExample() {
    const [counter, setCounter] = useState(0);
    const { isPending, execute, data } = useServerAction(incrementNumberAction); 

    return (
        <Card>
            <CardHeader>
                <CardTitle>Increment Number</CardTitle>
            </CardHeader>
            <CardContent className="flex flex-col gap-4">
                <Button
                    disabled={isPending} 
                    onClick={async () => {
                        const [data, err] = await execute({ 
                            number: counter, 
                        }) 

                        if (err) {
                            // handle error
                            return
                        }

                        setCounter(data);
                    }}
                >
                    Invoke action
                </Button>
                <p>Count:</p>
                <div>{isPending ? "saving..." : data}</div>
            </CardContent>
        </Card>
    );
}
  • useServerAction allows you to use this server action from within your client components.
  • execute executes the server action endpoint with the typesafe input directly from onClick.

Here is the result:

Increment Number

Count:

Thats just the beginning... Continue reading to learn more about zsa!

To use server actions for querying/refetching data from the client side, visit the Client Side Usage section.