Skip to main content

Customer Experience

Next.js Form Validation Using Server Actions by Zod

Blue and green data points moving in curves.

Server-side validation protects against invalid and malicious data, ensuring data integrity and security. In this post, we’ll look at how to utilize Zod, a declarative JavaScript validation library, for server-side form validation in a Next.js application. We will also explore how to handle validation failures provided by the server.

Assumptions

This blog assumes you have Next.js set up locally. If you don’t, please follow the official setup guide on the Next.js website: Next.js Setup Guide.

Prerequisites

In this blog post, I am using:
– Next.js 14.x
– Tailwind CSS
– TypeScript
– Zod (an NPM package for schema declaration and validation library for TypeScript)

Why Add Server-Side Validation?

Assume we’ve built a form with Next.js and added client-side validation. This form shows validation when the browser’s JavaScript is enabled but not when it is disabled. Without server-side validation, the form will not initiate validation and will submit the form. To prevent users from entering invalid data, we use server-side validation.

Here’s a screenshot showing how to disable JavaScript for a particular site in Chrome:

Chrome Js Disable

 

Below is a screenshot of my codebase. Once you have set up Next.js, you only need to create a components folder and an action.ts file, which should be parallel to the components folder.

Next.js Form Validation Using Server Actions by Zod

Creating the SignUp.tsx Component

In the components folder, create SignUp.tsx:

"use client";

import { FC, useState } from "react";
import { useFormState } from "react-dom";
import { createNewUser } from "@/action";

interface FormErrorProps {
errors?: string[];
}

const FormError: FC<FormErrorProps> = ({ errors }) => {
if (!errors?.length) return null;
return (
<div className="p-2">
{errors.map((err, index) => (
<p className="text-tiny text-red-400 list-item" key={index}>
{err}
</p>
))}
</div>
);
};

const SignUp: FC = () => {
const [date, setDate] = useState(new Date());
const [state, formAction] = useFormState(
createNewUser.bind(null, { date: date?.toISOString() }),
{ success: false }
);

return (
<form action={formAction} className="flex flex-col gap-y-2">
<label htmlFor="name">SignUp</label>
<input id="name" name="name" className="border-2" />
<FormError errors={state?.errors?.name} />

<input id="email" name="email" className="border-2" />
<FormError errors={state?.errors?.email} />

<input id="password" name="password" className="border-2" />
<FormError errors={state?.errors?.password} />

<button type="submit" className="border-2">
Create
</button>
</form>
);
};

export default SignUp;

 

Note: All form field elements can remain unchecked, and we don’t need to utilize React’s useState hook for the form state. In this component, we use the React form hook useFormState, which takes a function as its first argument. In our example, we are passing a function written in action.tsx and the state object for the form:

const [state, formAction] = useFormState(
createNewUser.bind(null, { date: date?.toISOString() }),
{ success: false }
);

 

CreateNewUser Function

We are still missing the createNewUser function used in the form’s action attribute. We’ll create this function in the action.ts file. This is a server action.

Server actions passed to the form action attribute receive the additional data, SignUpErrors, and FormData object of the form as the first argument, which encapsulates all the data of the form. We’ll use this object to get the value of the input fields and validate with Zod. In action.ts, I wrote the Zod schema for name, email, and password validation:

const userSchema = z.object({
name: z.string().trim().min(3, "Name must be at least 3 characters long!").max(18),
email: z.string().email("Email is invalid"),
password: z
.string()
.min(8, "Password is too short!")
.max(20, "Password is too long")
.regex(passwordValidation, {
message: "Your password is not valid",
})
.optional(),
});

 

Checking Form Data with safeParse()

In action.ts, we check the form data with safeParse(). If an error occurs, return an object with {success: false, errors: result.error.flatten().fieldErrors}, or on success, return {success: true}.

actions.ts

"use server";

import { z } from "zod";

/* TypeScript-first schema validation with static type inference */
const passwordValidation = new RegExp(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
);

const userSchema = z.object({
name: z.string().trim().min(3, "Name must be at least 3 characters long!").max(18),
email: z.string().email("Email is invalid"),
password: z
.string()
.min(8, "Password is too short!")
.max(20, "Password is too long")
.regex(passwordValidation, {
message: "Your password is not valid",
})
.optional(),
});

export interface SignUpErrors {
errors?: {
name?: string[];
email?: string[];
password?: string[];
};
success: boolean;
}

export const createNewUser = async (
date: any,
data: SignUpErrors,
formData: FormData
): Promise<SignUpErrors> => {
const result = userSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
password: formData.get("password"),
});
if (result.success) {
/* Database action like creating a user */
return { success: true };
}
return { success: false, errors: result.error.flatten().fieldErrors };
};

Note: We are using useFormState in SignUp.tsx. It only works in a Client Component, but none of its parents are marked with “use client”, so they are Server Components by default.

Conclusion

In this post, we covered the concept of Server Actions, their advantages over typical API routes, and using Server Actions in Next.js apps. Server Actions improve the user experience and development process by decreasing client-side JavaScript, increasing accessibility, and enabling server-side data modifications. If you want to create modern web applications with Next.js, Server Actions are a must-have tool. Experiment with Server Actions to improve your apps. Run the next bundle analyzer on a regular basis as part of your development process to keep track of changes in bundle size and detect any regressions.

 

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Rajiv Tandon

Rajiv Tandon is a Lead Technical Consultant at Perficient with over 8 years of experience in Front-end technologies. He has extensive knowledge of Insite, Magento 2, and other ecommerce platforms. He likes to seek knowledge and explore the latest front-end technologies.

More from this Author

Follow Us