Introduction
In web development, form validation is a crucial aspect of creating secure and user-friendly applications. It ensures that user inputs are correct, prevents malicious attacks, and improves the overall user experience. If you’re building applications with NextJS and looking for a robust way to handle form validation, Zod is an excellent library to consider.
This tutorial will guide you through the process of validating forms in NextJS using Zod, even complex forms with third-party components like date pickers and dropdowns. We’ll break down each step, making it easy for you to follow along and implement in your own projects.
Prerequisites
- Basic Web Development Knowledge: Familiarity with HTML, CSS, and JavaScript.
- React & NextJS: Basic understanding of React components and the NextJS framework.
- npm: Ability to install and manage packages using npm.
- Command Line Basics: Knowledge of using terminal commands to navigate directories and run scripts.
What You’ll Learn
- Validate Real-World Forms: Handle various input types, including dropdowns, date pickers, text inputs, autocomplete fields, multi-selects, text areas, and URL fields.
- Implement Comprehensive Validation: Secure your forms by effectively managing validation errors and protecting against malicious attacks.
- Validate Third-Party Components: Seamlessly validate forms that use popular libraries like React Datepicker and React Select.
- Handle Optional Fields: Validate optional fields only when users actually provide input, making your forms more flexible.
- Implement Conditional Validation: Make certain fields required based on the input in other fields, adding dynamic validation rules.
- Prevent Data Tampering: Protect your forms from malicious users attempting to manipulate form data.
- Validate URLs: Ensure that URL fields are in the correct format.
- Handle Errors Gracefully: Manage both validation errors and general errors (like database or session errors) to provide a smooth user experience.
Software We’ll Be Using
- Zod: A TypeScript-first schema validation library. (npmjs.com)
- React Datepicker: For date picker inputs. (npmjs.com)
- React Select: For autocomplete multi-select dropdowns. (React Select)
- Tailwind CSS: For styling your form (optional, but recommended for better presentation). (tailwindcss.com)
- NextJS: The React framework we’ll be building upon. (nextjs.org)
Getting Started
To make it easy for you to learn and practice, a GitHub repository is provided with “start” and “finish” folders. This allows you to code along step-by-step and check your progress against the completed code.
- Clone the Repository:
git clone https://github.com/codingmoney/nextjs-form-validation.git
- Navigate to the “start” Folder: Open the cloned repository in your code editor and navigate to the “start” folder. This folder contains the starting code for the tutorial.
- Install Dependencies: Run
npm install
in your terminal within the “start” folder to install all necessary packages. - Start the Development Server: Run
npm run dev
to start the NextJS development server. This will allow you to view the form in your browser as you build it.
Step-by-Step Tutorial: Form Validation in NextJS with Zod
Let’s dive into the step-by-step process of implementing form validation.
Step 1: Install and Import Zod
First, we need to install Zod in our NextJS project. Open your terminal in the project directory and run:
npm install zod
Once Zod is installed, import it into your component file where you will be handling the form.
import { z } from "zod";
Step 2: Create a Validation Schema with Zod
The core of Zod validation is creating a schema that defines the expected data structure and validation rules for your form inputs. Let’s consider some example fields:
- Recall Type: A dropdown with options “voluntary” or “mandatory”.
- Product Label: A text input that should be at least 3 characters long and contain only alphanumeric characters and spaces.
- Recall Overview: A text area requiring a minimum of 10 characters.
- Recall Date: A date input that needs to be validated as a date format.
- Recall Reason Types: A multi-select dropdown where at least one option must be selected.
- Business Website: An optional URL field that, if filled, must be a valid URL.
- Business Recall Date: Conditionally required if “Recall Type” is “voluntary”.
Create a new file config/zodSchema.js
Here’s how you can define a Zod schema for these fields:
import { z } from "zod";
import * as dropdown from "@/config/recallDropDowns";
const recallTypeSchema = z.enum(dropdown.recallTypeOptions, {
required_error: "Recall type is required.",
});
const productLabelSchema = z
.string()
.min(3)
.regex(/^[a-zA-Z0-9\s.,'%-]*$/, {
message: "Special characters not allowed.",
});
const overviewSchema = z
.string()
.min(10)
.regex(/^[a-zA-Z0-9\s.,'%\-\[\]\(\)]*$/, {
message: "Special characters not allowed.",
});
const recallDateRegex =
/^[A-Za-z]{3} [A-Za-z]{3} \d{2} \d{4} \d{2}:\d{2}:\d{2} GMT[+-]\d{4} \([A-Za-z ]+\)$/;
const recallReasonTypesSchema = z.string().refine(
(val) => {
try {
const parsed = JSON.parse(val); // Parse the JSON string
return Array.isArray(parsed) && parsed.length > 0; // Check if it's an array with at least 1 element
} catch (e) {
return false; // If parsing fails, return false
}
},
{
message: "You must select atleast 1 recall reason type.",
}
);
const urlSchema = z
.string()
.optional()
.refine(
(val) => {
if (!val) return true; // If the value is empty, it's valid
try {
new URL(val); // Check if the value is a valid URL
return true;
} catch {
return false; // If it's not a valid URL, return false
}
},
{
message: "Invalid URL format",
}
);
export const addRecallSchema = z
.object({
recall_type: recallTypeSchema,
biz_recall_date: z.string(),
product_label: productLabelSchema,
recall_reason_types: recallReasonTypesSchema,
overview: overviewSchema,
website: urlSchema,
})
.refine(
(data) => {
if (data.recall_type === "Voluntary") {
return (
!!data.biz_recall_date && recallDateRegex.test(data.biz_recall_date)
);
}
return true;
},
{
path: ["biz_recall_date"], // Point to the biz_recall_date field in case of an error
message:
"Recall date is required for voluntary recalls and must be in the format M/dd/yyyy.",
}
);
Explanation of the Schema:
z.object({})
: Defines a schema for a JavaScript object.z.enum(["voluntary", "mandatory"])
: Ensures therecallType
field can only be “voluntary” or “mandatory”.z.string()
: Specifies that the field should be a string..min(3, { message: ... })
: Sets a minimum length of 3 characters with a custom error message..regex(/^[a-zA-Z0-9\s]*$/, { message: ... })
: Uses a regular expression to allow only letters, numbers, and spaces in theproductLabel
.z.string().url().optional()
: ValidatesbusinessWebsite
as a URL, but makes it optional..refine(...)
: Allows for custom validation logic. In this case, it’s used for:recallReasonTypes
: To ensure it’s a JSON stringified array with at least one item.- Conditional validation for
businessRecallDate
: Making it required only whenrecallType
is “voluntary”.
Zod Schema Examples
Here are some common types and how you can define schemas for them:
1. Number
const numberSchema = z.number().min(1).max(100); // A number between 1 and 100
2. Boolean:
const booleanSchema = z.boolean(); // A boolean value (true or false)
3. Date:
const dateSchema = z.date().min(new Date('2023-01-01')); // A date after January 1st, 2023
4. Array:
const arraySchema = z.array(z.string()); // An array of strings
const numberArraySchema = z.array(z.number()).min(3); // An array of numbers with at least 3 items
5. Enum:
const roleEnum = z.enum(["Admin", "User", "Guest"]); // Enum for specific roles
6. Object:
const objectSchema = z.object({
name: z.string(),
age: z.number().min(18), // Nested validation within an object
});
7. Nullable and Optional:
const nullableSchema = z.string().nullable(); // Can be a string or null
const optionalSchema = z.string().optional(); // Can be a string or undefined
8. Custom Validation:
You can add custom validations using .refine():
const passwordSchema = z.string().refine((val) => val.length >= 6, {
message: "Password must be at least 6 characters long",
});
9. Union Types:
const stringOrNumber = z.union([z.string(), z.number()]); // Can be a string or a number
10. Transformations:
Zod also allows you to transform values:
const transformSchema = z.string().transform((val) => val.toUpperCase()); // Converts string to uppercase
11. Chaining Validators:
You can chain multiple validators:
const advancedSchema = z.string().min(5).max(10).regex(/^[a-zA-Z]+$/); // String between 5 and 10 characters containing only letters
Step 3: Validate Form Data with safeParse
In your form submission handler function app/actions/addRecall.js
, use the safeParse
method to validate the form data against your schema. safeParse
is used to avoid throwing errors and instead returns an object indicating success or failure.
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { addRecallSchema } from "@/config/zodSchema";
async function addRecall(formState, formData) {
const recallData = {
recall_type: formData.get("recall_type"),
biz_recall_date: formData.get("hid_biz_recall_date"),
product_label: formData.get("product_label"),
recall_reason_types: formData.get("hid_recall_reason_type"),
overview: formData.get("overview"),
website: formData.get("biz_website"),
};
const result = addRecallSchema.safeParse(recallData);
if (!result.success) {
console.log(result.error.flatten().fieldErrors);
console.log("biz recall date", recallData.biz_recall_date);
console.log(" recall type", recallData.recall_type);
return { errors: result.error.flatten().fieldErrors };
}
try {
//throw new Error("Failed to save data");
// Save to DB and Revalidate the cache
console.log("result data", result.data);
} catch (err) {
if (err instanceof Error) {
return {
errors: {
_form: [err.message],
},
};
} else {
return {
errors: {
_form: ["Something went wrong"],
},
};
}
}
//revalidatePath("/", "layout");
redirect(`/success`);
}
export default addRecall;
Explanation:
recallValidationSchema.safeParse(formData)
: Validates the form data extracted fromformData
against the schema.validatedFields.success
: A boolean indicating if validation was successful.validatedFields.error.flatten().fieldErrors
: If validation fails, this provides an object containing errors for each field.
Step 4: Display Validation Errors with useFormState
To display validation errors to the user, we use NextJS’s useFormState
hook. This hook allows you to manage state and actions related to your form, including displaying errors.
In the server action addRecall
function, if result.success
is false, we return an object containing the errors
. In the component, useFormState
hook provides the state
which includes the errors
object. You can then conditionally render error messages next to each form field in the components/RecallAddForm.jsx
.
{formState.errors.product_label ? (
<div className="p-2 bg-red-200 border border-red-400 rounded">
{formState.errors.product_label?.join(", ")}
</div>
) : null}
This code snippet checks if there’s an error for the productLabel
field in the formState.errors
object. If there is, it displays the error message. You should repeat this for each field you want to validate.
Benefits of Using Zod for Form Validation
- Seamless NextJS Integration: Zod works perfectly with NextJS, making form validation straightforward in your NextJS applications.
- TypeScript First: Being a TypeScript-first library, Zod provides excellent type safety and autocompletion, enhancing your development experience in TypeScript projects.
- Real-time User Feedback: Zod allows you to provide immediate feedback to users on input errors, improving the user experience.
- Enhanced Security and User Experience: Robust form validation with Zod significantly improves both the security and user-friendliness of your forms.
Conclusion
Form validation is essential for building robust and user-friendly web applications. By using Zod with NextJS, you can implement comprehensive and maintainable form validation with ease. This tutorial has provided a step-by-step guide to help you get started, covering everything from basic setup to handling complex scenarios like conditional validation and third-party components.
Start implementing Zod in your NextJS forms today and take your form validation to the next level! Don’t forget to explore the provided GitHub repository to see the complete code and further enhance your understanding. Happy coding!