Form validation is an important aspect of software development. This is because it ensures type safety and helps preserve data integrity by preventing your application users from submitting inaccurate or incomplete information.
Imagine a form asking for an exact number of guests, yet a user enters a range. In this scenario, your application is not expecting a range. But imagine again that the inaccurate data gets to your database. This is not a behavior you want from your application.
Validation helps prevent such mismatches. Form validation can be implemented on either the client side (using JavaScript) or the server side.
In this article, you will learn how to build type-safe forms in a TypeScript project using the powerful combination of React-Hook-Form library for form management and Zod for robust validation on the client side.
To follow along this article and understand the code, you will need to satisfy the following conditions:
In TypeScript, form validation uses the language's type system to create robust and reliable forms.
Form validation entails defining rules and checking for the data users enter into your application form. These rules (principles) can include various aspects, such as:
By implementing these validation checks, you prevent users from submitting incomplete or erroneous data, ultimately improving data quality and the functionality of your web application.
TypeScript's core strength lies in its type system. This system allows you to define the expected data types for variables, functions, and other aspects of your code. In the context of form validation, you can leverage types to define the structure of your form data.
For instance, you can create an interface or type alias that specifies the properties (fields) of your form and their corresponding data types.
Here are some common challenges developers often encounter in form validation and how TypeScript helps you fix them:
Zod is a powerful TypeScript-first schema declaration and validation library with static type inference for TypeScript applications, which allows you to define the expected structure (schema), data types, and validation rules for your application's inputs.
Zod is a lightweight, yet powerful validation library that enables you to define the structure of your data through schemas. Although TypeScript-first, it also supports JavaScript validation.
Zod supports many schema types, from primitive values like strings
, numbers
, and booleans
to complex types like objects
, arrays
, and tuples
. When TypeScript, React Hook Form, and Zod are combined, you can build robust and powerful forms. In the following sections, you will learn how to set up your form validation project.
If you wish to quickly get started, proceed with cloning the GitHub repository.
First, install create-next-app
globally by running:
npm i -g create-next-app
After that, you can use create-next-app CLI to create a Next app.
For this project, you will use Create Next App with the Typescript template to quickly bootstrap your React application. You will need to run this command to get started:
npx create-next-app@latest zod-ts-rhf --typescript --eslint
cd zod-ts-rhf
The command above:
zod-ts-rhf
--typescript
flag to specify the template to use; in this case TypeScript.cd zod-ts-rhf
changes directory into the zod-ts-rhf
folder.Ensure to make the following selection:
Inside the folder zod-ts-rhf
, update your tailwind.config.ts
with this code:
1import type { Config } from "tailwindcss";
2
3const config: Config = {
4 content: [
5 "./src/**/*.{js,ts,jsx,tsx,mdx}",
6 "./src/**/*.{js,ts,jsx,tsx,mdx}",
7 "./src/**/*.{js,ts,jsx,tsx,mdx}",
8 ],
9 mode: "jit",
10 theme: {
11 extend: {},
12 screens: {
13 xs: "480px",
14 ss: "620px",
15 sm: "768px",
16 md: "1060px",
17 lg: "1200px",
18 xl: "1700px",
19 },
20 },
21 plugins: [],
22};
23export default config;
The above code adds responsive breakpoints to Tailwind CSS.
Next, you need to install the zod
library and the React Hook Form resolver (@hookform/resolvers) by running this command inside the zod-ts-rhf
directory:
npm i @hookform/resolvers zod
In the next section, you will set up your form and learn how to get started using Zod.
First, start with creating a Form
component. To do this, create a components
folder in the src
directory, then create a Form.tsx
file in the components
directory, and then, paste this code:
We will be building a sign-up form with Zod validation. Paste the following code in the Form.tsx
file
1// ./src/components/Form.tsx
2
3import React from "react";
4
5export default function Form() {
6 return (
7 <div>
8 <div>
9 <div className="signup-1 flex items-center relative h-screen">
10 <div className="overlay absolute inset-0 z-0 bg-black opacity-75"></div>
11 <div className="container px-4 mx-auto relative z-10">
12 <div className="sm:w-10/12 md:w-8/12 lg:w-6/12 xl:w-5/12 mx-auto">
13 <div className="box bg-white p-6 md:px-12 md:pt-12 border-t-10 border-solid border-indigo-600">
14 <h2 className="text-3xl text-gray-800 text-center">
15 Create Your Account
16 </h2>
17
18 <form>
19 <div className="signup-form mt-6 md:mt-12">
20 <div className="border-2 border-solid rounded flex items-center mb-4">
21 <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
22 <span className="far fa-user text-gray-500"></span>
23 </div>
24 <div className="flex-1">
25 <input
26 type="text"
27 placeholder="Username"
28 className="text-gray-700 h-10 py-1 pr-3 w-full"
29 />
30 </div>
31 </div>
32
33 <div className="border-2 border-solid rounded flex items-center mb-4">
34 <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
35 <span className="far fa-envelope text-gray-500"></span>
36 </div>
37 <div className="flex-1">
38 <input
39 type="text"
40 placeholder="E-mail"
41 className="text-gray-700 h-10 py-1 pr-3 w-full"
42 />
43 </div>
44 </div>
45
46 <div className="border-2 border-solid rounded flex items-center mb-4">
47 <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
48 <span className="fas fa-asterisk text-gray-500"></span>
49 </div>
50 <div className="flex-1">
51 <input
52 type="password"
53 placeholder="Password"
54 className="text-gray-700 h-10 py-1 pr-3 w-full"
55 />
56 </div>
57 </div>
58
59 <div className="border-2 border-solid rounded flex items-center mb-4">
60 <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
61 <span className="far fa-user text-gray-500"></span>
62 </div>
63 <div className="flex-1">
64 <input
65 type="text"
66 placeholder="Full name"
67 className="text-gray-700 h-10 py-1 pr-3 w-full"
68 />
69 </div>
70 </div>
71
72 <div className="border-2 border-solid rounded flex items-center mb-4">
73 <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
74 <span className="fa fa-hashtag text-gray-500"></span>
75 </div>
76 <div className="flex-1">
77 <input
78 type="number"
79 placeholder="Age"
80 className="text-gray-700 h-10 py-1 pr-3 w-full"
81 />
82 </div>
83 </div>
84
85 <p className="text-sm text-center mt-6">
86 By signing up, you agree to our{" "}
87 <a href="#" className="text-indigo-600 hover:underline">
88 Terms
89 </a>{" "}
90 and{" "}
91 <a href="#" className="text-indigo-600 hover:underline">
92 Privacy Policy
93 </a>
94 </p>
95
96 <div className="text-center mt-6 md:mt-12">
97 <button className="bg-indigo-600 hover:bg-indigo-700 text-white text-xl py-2 px-4 md:px-6 rounded transition-colors duration-300">
98 Sign Up{" "}
99 <span className="far fa-paper-plane ml-2"></span>
100 </button>
101 </div>
102 </div>
103 </form>
104
105 <div className="border-t border-solid mt-6 md:mt-12 pt-4">
106 <p className="text-gray-500 text-center">
107 Already have an account,{" "}
108 <a href="#" className="text-indigo-600 hover:underline">
109 Sign In
110 </a>
111 </p>
112 </div>
113 </div>
114 </div>
115 </div>
116 </div>
117 </div>
118 </div>
119 );
120}
Then, update the index.tsx
file in the ./src/pages/index.tsx
file with this code:
1import Form from "@/components/Form";
2
3export default function Home() {
4 return (
5 <>
6 <Form />
7 </>
8 );
9}
The code snippets above renders the Form
component in the index.tsx
file corresponding to the index
page since you are making use of the Next.js Pages Router. Subsequently, in this article, you will only focus on working on the Form.tsx
file.
Start your app by running the command below:
npm run dev
You should see your Sign-up form by navigating to http://localhost:3000:
In the next sections, you will walk through creating schema definitions for form fields and also, handling complex validation scenarios with Zod.
To get started with Zod validation in your form, you will need to import a couple of things at the beginning of your Form.tsx
file:
zodResolver
(from @hookform/resolvers/zod
): This allows you to connect Zod's validation with React Hook Form. It expects your defined schema as input.z (from zod)
: This import gives you access to all the functionalities and functions offered by Zod for building your validation schema.1// ./src/components/Form.tsx
2import { zodResolver } from '@hookform/resolvers/zod';
3import * as z from 'zod';
4
5...
Next, define your form schema. This form will have the following form fields:
username
,email
,password
,fullName
,age
1import { zodResolver } from "@hookform/resolvers/zod";
2import * as z from "zod";
3
4const FormSchema = z.object({
5 username: z.string(),
6 email: z.string(),
7 password: z.string(),
8 fullName: z.string(),
9 age: z.number(),
10});
11
12type IFormInput = z.infer<typeof FormSchema>;
13
14...
The code snippet above uses Zod to define FormSchema
to define a schema object that has five form fields with the following data types:
username
: string,email
: string,password
: string,fullName
: string,age
: numberThe IFormInput
type uses Zod type inference to statically infer the TypeScript type from the FormSchema
schema. This is an added advantage provided by Zod, which allows you to infer types rather than explicitly defining them.
Right now, the validation check by Zod is a simple one, it defines the data type for each form field.
In the next section, you will walk you through how to use Zod to add complex validation checks like defining the minimum and maximum length for the password
field, checking that the username
does not contain special characters, and checking that the age
falls within a range.
By hovering over IFormInput
in your code editor, you should see that IFormInput
matches your Zod FormSchema
schema.
In this section, you will update the FormSchema
schema to check for complex validations. To do that, update the FormSchema
schema with this code below:
1const FormSchema = z.object({
2 username: z
3 .string()
4 .min(3, "Username must not be lesser than 3 characters")
5 .max(25, "Username must not be greater than 25 characters")
6 .regex(
7 /^[a-zA-Z0-9_]+$/,
8 "The username must contain only letters, numbers and underscore (_)",
9 ),
10 email: z.string().email("Invalid email. Email must be a valid email address"),
11 password: z
12 .string()
13 .min(3, "Password must not be lesser than 3 characters")
14 .max(16, "Password must not be greater than 16 characters"),
15 fullName: z.string().min(3, "Name must not be lesser than 3 characters"),
16 age: z.string().refine(
17 (age) => {
18 return Number(age) >= 18;
19 },
20 { message: "You must be 18 years or older" },
21 ),
22});
In the above code, you are using the different Zod validation functions such as min()
, max()
, string()
, email()
, .refine()
, and regex()
to validate the different form fields. These functions also accept custom error messages in case the validation does not pass.
Specifically, you are doing the following for these form fields:
username
: You are using the min()
function to make sure the characters are not below three and, then the max()
function checks that the characters are not beyond twenty-five. You also make use of Regex to make sure the username must contain only letters, numbers, and underscoreemail
: You use the email()
function from Zod functions to check that the email
is valid.password
: Just like in the case of username
, you are checking for the length using the min()
and max()
functions.fullName
: You are using the min()
function to set a minimum number of characters expected.age
: you will use of the string()
function, because, even though age
should be a number. This is because Zod sees the values from the age input as strings. However, you will limit the values to number
in the age input by using the <input type="number" />
. You then use the Zod .refine()
function to customize the validation logic to check that the age
is greater than 18.Up until this section of the article, you have not started using both the IFormInput
type and the FormSchema
schema. In this section, you will learn how to pass the Zod schema to React Hook Form using the useForm
hook:
Update the Form.tsx
file with the following code, like so:
1import { useForm } from "react-hook-form";
2...
3
4export default function Form() {
5 ...
6
7 const {
8 register,
9 handleSubmit,
10 formState: { errors },
11 } = useForm<IFormInput>({
12 resolver: zodResolver(FormSchema),
13 });
14
15 const onSubmit = (data: IFormInput) => {
16 console.log(data);
17 };
18
19 return (
20 <div className="signup-1 flex items-center relative h-screen">
21 <div className="overlay absolute inset-0 z-0 bg-black opacity-75"></div>
22 <div className="container px-4 mx-auto relative z-10">
23 <div className="sm:w-10/12 md:w-8/12 lg:w-6/12 xl:w-5/12 mx-auto">
24 <div className="box bg-white p-6 md:px-12 md:pt-12 border-t-10 border-solid border-indigo-600">
25 <h2 className="text-3xl text-gray-800 text-center">
26 Create Your Account
27 </h2>
28
29 <form onSubmit={handleSubmit(onSubmit)}>
30 <div className="signup-form mt-6 md:mt-12">
31 <div className="border-2 border-solid rounded flex items-center mb-4">
32 <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
33 <span className="far fa-user text-gray-500"></span>
34 </div>
35 <div className="flex-1">
36 <input
37 {...register("username")}
38 type="text"
39 placeholder="Username"
40 className="text-gray-700 h-10 py-1 pr-3 w-full"
41 />
42 </div>
43 </div>
44 {errors?.username?.message && (
45 <p className="text-red-700 mb-4">{errors.username.message}</p>
46 )}
47
48 <div className="border-2 border-solid rounded flex items-center mb-4">
49 <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
50 <span className="far fa-envelope text-gray-500"></span>
51 </div>
52 <div className="flex-1">
53 <input
54 {...register("email")}
55 type="text"
56 placeholder="E-mail"
57 className="text-gray-700 h-10 py-1 pr-3 w-full"
58 />
59 </div>
60 </div>
61 {errors?.email?.message && (
62 <p className="text-red-700 mb-4">{errors.email.message}</p>
63 )}
64
65 <div className="border-2 border-solid rounded flex items-center mb-4">
66 <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
67 <span className="fas fa-asterisk text-gray-500"></span>
68 </div>
69 <div className="flex-1">
70 <input
71 {...register("password")}
72 type="password"
73 placeholder="Password"
74 className="text-gray-700 h-10 py-1 pr-3 w-full"
75 />
76 </div>
77 </div>
78 {errors?.password?.message && (
79 <p className="text-red-700 mb-4">{errors.password.message}</p>
80 )}
81
82 <div className="border-2 border-solid rounded flex items-center mb-4">
83 <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
84 <span className="far fa-user text-gray-500"></span>
85 </div>
86 <div className="flex-1">
87 <input
88 {...register("fullName")}
89 type="text"
90 placeholder="Full name"
91 className="text-gray-700 h-10 py-1 pr-3 w-full"
92 />
93 </div>
94 </div>
95
96 {errors?.fullName?.message && (
97 <p className="text-red-700 mb-4">{errors.fullName.message}</p>
98 )}
99
100 <div className="border-2 border-solid rounded flex items-center mb-4">
101 <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
102 <span className="fa fa-hashtag text-gray-500"></span>
103 </div>
104 <div className="flex-1">
105 <input
106 {...register("age")}
107 type="number"
108 placeholder="Age"
109 className="text-gray-700 h-10 py-1 pr-3 w-full"
110 />
111 </div>
112 </div>
113 {errors?.age?.message && (
114 <p className="text-red-700 mb-4">{errors.age.message}</p>
115 )}
116
117 <div className="text-center mt-6 md:mt-12">
118 <button
119 className="bg-indigo-600 hover:bg-indigo-700 text-white text-xl py-2 px-4 md:px-6 rounded transition-colors duration-300"
120 onClick={handleSubmit(onSubmit)}
121 >
122 Sign Up <span className="far fa-paper-plane ml-2"></span>
123 </button>
124 </div>
125 </div>
126 </form>
127 </div>
128 </div>
129 </div>
130 </div>
131 );
132}
In the above code, you:
useForm
from the react-hook-form library. Other imports remain unchanged such as z
and zodResolver
.useForm
hook
pass a configuration object to the useForm hook as an argument
set the resolver property to the result of calling the zodResolver
function with the FormSchema
as an argumentonSubmit()
function as an argument to the React Hook Form handleSubmit()
function and assigned it to the form's onSubmit
event.The useForm
hook returns a couple of properties to manage your form. While it offers a wider range of functionalities, you will focus on three key ones for our Zod validation setup:
register
: This registers your form's input. By using register
, you tell React Hook Form to track the values entered by the input elements and integrate Zod's validation for them.formState
: This information about your entire form's state. It includes details like whether any validation errors.handleSubmit
: This function is fired when the user submits the form. But here's the catch: handleSubmit
only receives the form data if it successfully passes Zod's validation checks. This way, you can be sure the data is clean before processing it further.Running the application, you should see similar to the GIF below:
In this article, you learned form validation using TypeScript, React Hook Form, and Zod. Here's a quick recap of the valuable takeaways:
You now understand the power of Zod for defining a schema – a blueprint for your form data. This schema controls the structure and data types for each form field, ensuring consistency and catching potential errors early. You also learned how Zod allows you to add validation logic, keeping your user-submitted data clean and reliable. By combining React Hook Form and Zod, you now know how to create robust and user-friendly forms.
Check out the complete code for this article on this GitHub repository. Check out the deployed site live.
If you still want to explore more on this topic, links to relevant articles and documentation are included below.
A passionate software engineer with a front-end development background (React JS, Svelte, Vue.js, Micro-frontends) thrives on building user-friendly applications. Leverages technical writing skills to craft clear documentation and fosters collaboration across teams with a DevOps mindset.