Next.js Server Actions with next-safe-action
Tags: nextjs, typesafety, validation, serveractions
Published: 2024-06-25 20:15:46

---
title: Next.js Server Actions with next-safe-action
published: true
date: 2024-06-18 00:00:00 UTC
tags: nextjs,typesafety,validation,serveractions
canonical_url: https://www.davegray.codes/posts/nextjs-server-actions-with-next-safe-action
cover_image: https://raw.githubusercontent.com/gitdagray/my-blogposts/main/images/nextjs-server-actions-with-next-safe-action.png
---


**TLDR:** Add type safe and validated server actions to your Next.js App Router project with next-safe-action.

## Next.js Server Actions

[Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) are asynchronous functions executed on the server in Next.js. 

They are defined with the `"use server"` directive and can be used 
in both server and client components for handling form submissions and data mutations.

Over the last year, I've seen them applied in a variety of ways, and I've used them in projects myself, too.

Now, I have recently discovered the [next-safe-action](https://next-safe-action.dev/) library, and I like the structure, ease-of-use, and extra features it provides. 

## An Example Server Action without next-safe-action

I think the best way to show why I like [next-safe-action](https://next-safe-action.dev/) is to show how I implemented a Next.js server action without the library first. 

Afterwards, I will show the refactor with [next-safe-action](https://next-safe-action.dev/).

Here's an example server action from a repository and tutorial I recently published on creating a [Next.js Modal Form with react-hook-form, ShadCN/ui, Server Actions and Zod validation](https://youtu.be/WyL_Jc6_-sY).


```ts
// src/app/actions/actions.ts
"use server"

import { UserSchema } from "@/schemas/User"
import type { User } from "@/schemas/User"

type ReturnType = {
    message: string,
    errors?: Record<string, unknown>
}

export async function saveUser(user: User): Promise<ReturnType> {

    // Check valid login here
    
    const parsed = UserSchema.safeParse(user)

    if (!parsed.success) {
        return {
            message: "Submission Failed",
            errors: parsed.error.flatten().fieldErrors
        }
    }

    await fetch(`http://localhost:3500/users/${user.id}`, {
        method: 'PATCH',
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            firstname: user.firstname,
            lastname: user.lastname,
            email: user.email,
        })
    })

    return { message: "User Updated! 🎉" }
}
```
You can see above that I was using a local `json-server` instance in the tutorial to update user data. 

Before the update occurs, the data is validated with [Zod](https://zod.dev/). 

If the validation fails, Zod validation errors are sent back to the client component with the ZodError type `flatten` method applied. 

Now let's compare to the refactored version using [next-safe-action](https://next-safe-action.dev/).

## An Example Server Action with next-safe-action

```ts 
// src/app/actions/actions.ts
"use server"

import { UserSchema } from "@/schemas/User"
import { actionClient } from "@/lib/safe-action"
import { flattenValidationErrors } from "next-safe-action"

export const saveUserAction = actionClient
    .schema(UserSchema, {
        handleValidationErrorsShape: (ve) => flattenValidationErrors(ve).fieldErrors,
    })
    .action(async ({ parsedInput: { id, firstname, lastname, email } }) => {

        // Check valid login here

        await fetch(`http://localhost:3500/users/${id}`, {
            method: 'PATCH',
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                firstname: firstname,
                lastname: lastname,
                email: email,
            })
        })

        return { message: "User Updated! 🎉" }
    })
```
The file has shrunk down from 33 lines to 26 lines of code.

Starting at the top you can see I still import the Zod UserSchema I have defined. The inferred User type is no longer imported. 

New imports include `actionClient` and `flattenValidationErrors`. 

Instead of `export async function`, I'm using `export const` and starting the definition of `saveUserAction` with the `actionClient`. 

I chain the `schema` method to the `actionClient` while passing in the `UserSchema`. I also set the `handleValidationErrorsShape` option to use the imported `flattenValidationErrors` method. This method is similar to the ZodError type method `flatten` that I used in the original function.

Next, I chain the `action` method and call the async function inside of it. It supplies a `parsedInput` prop. I destructure the prop to get the input data sent to the server action. 

The remainder of the function remains unchanged. 

Note that in this refactored version I did not define a `ReturnType`. 

The return type is the result defined by the [useAction hook return object](https://next-safe-action.dev/docs/execution/hooks/useaction#useaction-return-object). I apply the `useAction` hook in the client component. 

While some overhead is saved in the server action code you see above, even more is saved in the client component. 

Below, I again show before and after code versions. This time the before and afters are of the client component using [react-hook-form](https://react-hook-form.com/).

## An Example Client Component without next-safe-action
 
```ts 
// src/app/edit/[id]/UserForm.tsx
"use client"

import { useForm } from "react-hook-form"
import { Form } from "@/components/ui/form"
import { Button } from "@/components/ui/button"
import { InputWithLabel } from "@/components/InputWithLabel"
import { zodResolver } from "@hookform/resolvers/zod"
import { UserSchema } from "@/schemas/User"
import type { User } from "@/schemas/User"
import { saveUser } from "@/app/actions/actions"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"

type Props = {
    user: User
}

export default function UserForm({ user }: Props) {
    const [message, setMessage] = useState('')
    const [errors, setErrors] = useState({})
    const router = useRouter()

    const form = useForm<User>({
        mode: 'onBlur',
        resolver: zodResolver(UserSchema),
        defaultValues: { ...user },
    })

    useEffect(() => {
        // boolean to indicate if form has not been saved
        localStorage.setItem("userFormModified", form.formState.isDirty.toString())
    }, [form.formState.isDirty])

    async function onSubmit() {
        setMessage('')
        setErrors({})
        /* No need to validate here because 
        react-hook-form already validates with 
        the Zod schema */
        const result = await saveUser(form.getValues())
        if (result?.errors) {
            setMessage(result.message)
            setErrors(result.errors)
            return
        } else {
            setMessage(result.message)
            // update client-side cache
            router.refresh() 
            // reset dirty fields
            form.reset(form.getValues())
        }
    }

    return (
        <div>
            {message ? (
                <h2 className="text-2xl">{message}</h2>
            ) : null}

            {errors ? (
                <div className="mb-10 text-red-500">
                    {Object.keys(errors).map(key => (
                        <p key={key}>{`${key}: ${errors[key as keyof typeof errors]}`}</p>
                    ))}
                </div>
            ) : null}

            <Form {...form}>
                <form onSubmit={(e) => {
                    e.preventDefault()
                    form.handleSubmit(onSubmit)();
                }} className="flex flex-col gap-4">

                    <InputWithLabel
                        fieldTitle="First Name"
                        nameInSchema="firstname"
                    />
                    <InputWithLabel
                        fieldTitle="Last Name"
                        nameInSchema="lastname"
                    />
                    <InputWithLabel
                        fieldTitle="Email"
                        nameInSchema="email"
                    />
                    <div className="flex gap-4">
                        <Button>Submit</Button>
                        <Button
                            type="button"
                            variant="destructive"
                            onClick={() => form.reset()}
                        >Reset</Button>
                    </div>
                </form>
            </Form>
        </div>
    )
}
```
In the above example, I had to set state for both the message and errors that the original server action could return. 

I also needed to consider that state in the onSubmit function. 

In the refactored version below, you can see how this is simplified.

## An Example Client Component with next-safe-action

```ts
// src/app/edit/[id]/UserForm.tsx
"use client"

import { useForm } from "react-hook-form"
import { Form } from "@/components/ui/form"
import { Button } from "@/components/ui/button"
import { InputWithLabel } from "@/components/InputWithLabel"
import { zodResolver } from "@hookform/resolvers/zod"
import { UserSchema } from "@/schemas/User"
import type { User } from "@/schemas/User"
import { saveUserAction } from "@/app/actions/actions"
import { useEffect } from "react"
import { useRouter } from "next/navigation"

import { useAction } from "next-safe-action/hooks"
import { DisplayServerActionResponse } from "@/components/DisplayServerActionResponse"

type Props = {
    user: User
}

export default function UserForm({ user }: Props) {
    const router = useRouter()
    const { execute, result, isExecuting } = useAction(saveUserAction)

    const form = useForm<User>({
        resolver: zodResolver(UserSchema),
        defaultValues: { ...user },
    })

    useEffect(() => {
        // boolean to indicate if form has not been saved
        localStorage.setItem("userFormModified", form.formState.isDirty.toString())
    }, [form.formState.isDirty])

    async function onSubmit() {
        /* No need to validate here because 
        react-hook-form already validates with 
        the Zod schema */
        execute(form.getValues())
        // update client-side cache
        router.refresh()
        // reset dirty fields
        form.reset(form.getValues())
    }

    return (
        <div>
            <DisplayServerActionResponse result={result} />

            <Form {...form}>
                <form onSubmit={(e) => {
                    e.preventDefault()
                    form.handleSubmit(onSubmit)();
                }} className="flex flex-col gap-4">

                    <InputWithLabel
                        fieldTitle="First Name"
                        nameInSchema="firstname"
                    />
                    <InputWithLabel
                        fieldTitle="Last Name"
                        nameInSchema="lastname"
                    />
                    <InputWithLabel
                        fieldTitle="Email"
                        nameInSchema="email"
                    />
                    <div className="flex gap-4">
                        <Button>{isExecuting ? "Working..." : "Submit"}</Button>
                        <Button
                            type="button"
                            variant="destructive"
                            onClick={() => form.reset()}
                        >Reset</Button>
                    </div>
                </form>
            </Form>
        </div>
    )
}
```
In this refactored version, I imported the [useAction](https://next-safe-action.dev/docs/execution/hooks/useaction) hook supplied by next-safe-action and a custom component I created called `DisplayServerActionResponse`.

I eliminated all usage of `useState`.

`DisplayServerActionResponse` receives the `result` that is provided by the useAction hook. It holds the data sent back from the server action. 

`useAction` also provides an `execute` function and an `isExecuting` boolean. (Check the [docs](https://next-safe-action.dev/docs/introduction) for what else it can provide, too.)

All of this greatly reduces the logic I needed to put in the `onSubmit` function. 

Receiving the `result` from the server action makes it easy to abstract the displayed response to the custom `DisplayServerActionResponse` component, too. 

Here's a quick look at that component as well..

## Displaying the Server Action Result 

```ts
type Props = {
    result: {
        data?: {
            message?: string,
        },
        serverError?: string,
        fetchError?: string,
        validationErrors?: Record<string, string[] | undefined> | undefined,
    }
}

export function DisplayServerActionResponse({ result }: Props) {

    const { data, serverError, fetchError, validationErrors } = result

    return (
        <>
            {/* Success Message */}
            {data?.message ? (
                <h2 className="text-2xl my-2">{data.message}</h2>
            ) : null}

            {serverError ? (
                <div className="my-2 text-red-500">
                    <p>{serverError}</p>
                </div>
            ) : null}

            {fetchError ? (
                <div className="my-2 text-red-500">
                    <p>{fetchError}</p>
                </div>
            ) : null}

            {validationErrors ? (
                <div className="my-2 text-red-500">
                    {Object.keys(validationErrors).map(key => (
                        <p key={key}>{`${key}: ${validationErrors && validationErrors[key as keyof typeof validationErrors]}`}</p>
                    ))}
                </div>
            ) : null}
        </>
    )
}
```
Above, you can see that [next-safe-action](https://next-safe-action.dev/) provides not only validation errors from the Zod schema I constructed, but it also provides server errors and fetch errors. 

In addition, the result object contains the success message I provided from the server action. 

## Learn More

This is just one example and a simple one at that! Dive into the docs and solve your own specific use case to see what else [next-safe-action](https://next-safe-action.dev/) is capable of. 

I plan to refactor my old server actions and use next-safe-action going forward. 

<hr />

## Let's Connect!

Hi, I'm Dave. I work as a full-time developer, instructor and creator. 

If you enjoyed this article, you might enjoy my other content, too.

**My Stuff:** [Courses, Cheat Sheets, Roadmaps](https://courses.davegray.codes/)

**My Blog:** [davegray.codes](https://www.davegray.codes/)

**YouTube:**  [@davegrayteachescode](https://www.youtube.com/davegrayteachescode) 

**X:**  [@yesdavidgray](https://x.com/yesdavidgray)  

**GitHub:** [gitdagray](https://github.com/gitdagray)

**LinkedIn:**  [/in/davidagray](https://www.linkedin.com/in/davidagray/) 

**Patreon:**  [Join my Support Team!](patreon.com/davegray)

**Buy Me A Coffee:**  [You will have my sincere gratitude](https://www.buymeacoffee.com/davegray)   

Thank you for joining me on this journey. 

Dave