Knower
Auth
Creating Validation Schema

Validation Schema (Signup & Login)

Introduction

I wanted my frontend let users know the form errors when they typed the wrong input while signing up or logging in.

For this, I used Zod (opens in a new tab) library instead of react-hook-form which is more typescript oriented and effectively controls the validation schema.

There are also other substitutes for Zod and you can check in here (opens in a new tab) for their comparison.

Installation & Configuration

Installation

Let's install (opens in a new tab) package.


npm i zod
yarn add zod
pnpm add zod
bun add zod

Editing tsconfig.json

As Zod library is very typescript oriented, we need to fix the tsconfig.json as shown in the requirements (opens in a new tab) to maximize its usage.


{
"compilerOptions": {
"strict": true
}
}

Making Schema File (Signup)

Creating Schema & Type

I created schema variable called signupSchema. As data will be in object type, I used .object() (opens in a new tab) and objectified them.


import { z } from "zod";
export const signupSchema = z.object();
export type SignupInput = z.infer<typeof signupSchema>;

firstName & lastName

Basically I made first name and last name as required value. Therefore, I used .nonempty() (opens in a new tab) for marking them as required, and showed error message if they didn't pass the validation.


import { z } from "zod";
export const signupSchema = z
.object({
firstName: z.string().nonempty({ message: "First name is required" }),
lastName: z.string().nonempty({ message: "Last name is required" }),
email: z
.string({
required_error: "Email is required",
})
.email({ message: "Invalid email address" }),
phoneNumber: z
.string()
.regex(/^[0-9]+$/, { message: "Invalid phone number form" }),
password: z
.string({ required_error: "Password is required" })
.min(8, { message: "Should not be less than 8 characters" })
.max(16, { message: "Should not be no more than 16 characters" }),
confirmPassword: z
.string()
.nonempty({ message: "You need to confirm password" }),
role: z.string().min(1, { message: "Please choose your role" }),
addressFirst: z.string().optional(),
addressSecond: z.string().optional(),
city: z.string().optional(),
country: z.string().optional(),
zipCode: z.string().optional(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Password do not match",
});
export type SignupInput = z.infer<typeof signupSchema>;

email

With using .email() (opens in a new tab) within .string(), I could easily check whether email address is in valid format or not.


import { z } from "zod";
export const signupSchema = z
.object({
firstName: z.string().nonempty({ message: "First name is required" }),
lastName: z.string().nonempty({ message: "Last name is required" }),
email: z
.string({
required_error: "Email is required",
})
.email({ message: "Invalid email address" }),
phoneNumber: z
.string()
.regex(/^[0-9]+$/, { message: "Invalid phone number form" }),
password: z
.string({ required_error: "Password is required" })
.min(8, { message: "Should not be less than 8 characters" })
.max(16, { message: "Should not be no more than 16 characters" }),
confirmPassword: z
.string()
.nonempty({ message: "You need to confirm password" }),
role: z.string().min(1, { message: "Please choose your role" }),
addressFirst: z.string().optional(),
addressSecond: z.string().optional(),
city: z.string().optional(),
country: z.string().optional(),
zipCode: z.string().optional(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Password do not match",
});
export type SignupInput = z.infer<typeof signupSchema>;

phoneNumber

At first I thought phoneNumber should be a number but ended up saving it as a string type for unifying types of signup inputs.

So I used .regex() to check whether it contains other strings (including space) other than numbers.


import { z } from "zod";
export const signupSchema = z
.object({
firstName: z.string().nonempty({ message: "First name is required" }),
lastName: z.string().nonempty({ message: "Last name is required" }),
email: z
.string({
required_error: "Email is required",
})
.email({ message: "Invalid email address" }),
phoneNumber: z
.string()
.regex(/^[0-9]+$/, { message: "Invalid phone number form" }),
password: z
.string({ required_error: "Password is required" })
.min(8, { message: "Should not be less than 8 characters" })
.max(16, { message: "Should not be no more than 16 characters" }),
confirmPassword: z
.string()
.nonempty({ message: "You need to confirm password" }),
role: z.string().min(1, { message: "Please choose your role" }),
addressFirst: z.string().optional(),
addressSecond: z.string().optional(),
city: z.string().optional(),
country: z.string().optional(),
zipCode: z.string().optional(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Password do not match",
});
export type SignupInput = z.infer<typeof signupSchema>;

password

I used .min() and .max() to set minimum and maximum characters of password, respectively.


import { z } from "zod";
export const signupSchema = z
.object({
firstName: z.string().nonempty({ message: "First name is required" }),
lastName: z.string().nonempty({ message: "Last name is required" }),
email: z
.string({
required_error: "Email is required",
})
.email({ message: "Invalid email address" }),
phoneNumber: z
.string()
.regex(/^[0-9]+$/, { message: "Invalid phone number form" }),
password: z
.string({ required_error: "Password is required" })
.min(8, { message: "Should not be less than 8 characters" })
.max(16, { message: "Should not be no more than 16 characters" }),
confirmPassword: z
.string()
.nonempty({ message: "You need to confirm password" }),
role: z.string().min(1, { message: "Please choose your role" }),
addressFirst: z.string().optional(),
addressSecond: z.string().optional(),
city: z.string().optional(),
country: z.string().optional(),
zipCode: z.string().optional(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Password do not match",
});
export type SignupInput = z.infer<typeof signupSchema>;

confirmPassword

With using .refine() (opens in a new tab), I checked whether confirmPassword is as same as password, and if not I made it threw an error message.


import { z } from "zod";
export const signupSchema = z
.object({
firstName: z.string().nonempty({ message: "First name is required" }),
lastName: z.string().nonempty({ message: "Last name is required" }),
email: z
.string({
required_error: "Email is required",
})
.email({ message: "Invalid email address" }),
phoneNumber: z
.string()
.regex(/^[0-9]+$/, { message: "Invalid phone number form" }),
password: z
.string({ required_error: "Password is required" })
.min(8, { message: "Should not be less than 8 characters" })
.max(16, { message: "Should not be no more than 16 characters" }),
confirmPassword: z
.string()
.nonempty({ message: "You need to confirm password" }),
role: z.string().min(1, { message: "Please choose your role" }),
addressFirst: z.string().optional(),
addressSecond: z.string().optional(),
city: z.string().optional(),
country: z.string().optional(),
zipCode: z.string().optional(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Password do not match",
});
export type SignupInput = z.infer<typeof signupSchema>;

role

As I let users to choose their role in their frontend with using <select> and <option> tags, I set .min() to be 1 so that it could be the required value (Default value is empty string).


import { z } from "zod";
export const signupSchema = z
.object({
firstName: z.string().nonempty({ message: "First name is required" }),
lastName: z.string().nonempty({ message: "Last name is required" }),
email: z
.string({
required_error: "Email is required",
})
.email({ message: "Invalid email address" }),
phoneNumber: z
.string()
.regex(/^[0-9]+$/, { message: "Invalid phone number form" }),
password: z
.string({ required_error: "Password is required" })
.min(8, { message: "Should not be less than 8 characters" })
.max(16, { message: "Should not be no more than 16 characters" }),
confirmPassword: z
.string()
.nonempty({ message: "You need to confirm password" }),
role: z.string().min(1, { message: "Please choose your role" }),
addressFirst: z.string().optional(),
addressSecond: z.string().optional(),
city: z.string().optional(),
country: z.string().optional(),
zipCode: z.string().optional(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Password do not match",
});
export type SignupInput = z.infer<typeof signupSchema>;

Fields For Address

I set all fields for addresss as optional with using .optional() (opens in a new tab).


import { z } from "zod";
export const signupSchema = z
.object({
firstName: z.string().nonempty({ message: "First name is required" }),
lastName: z.string().nonempty({ message: "Last name is required" }),
email: z
.string({
required_error: "Email is required",
})
.email({ message: "Invalid email address" }),
phoneNumber: z
.string()
.regex(/^[0-9]+$/, { message: "Invalid phone number form" }),
password: z
.string({ required_error: "Password is required" })
.min(8, { message: "Should not be less than 8 characters" })
.max(16, { message: "Should not be no more than 16 characters" }),
confirmPassword: z
.string()
.nonempty({ message: "You need to confirm password" }),
role: z.string().min(1, { message: "Please choose your role" }),
addressFirst: z.string().optional(),
addressSecond: z.string().optional(),
city: z.string().optional(),
country: z.string().optional(),
zipCode: z.string().optional(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Password do not match",
});
export type SignupInput = z.infer<typeof signupSchema>;

Type Export

I created type of SignupInput that could be matched up with signupSchema.


import { z } from "zod";
export const signupSchema = z
.object({
firstName: z.string().nonempty({ message: "First name is required" }),
lastName: z.string().nonempty({ message: "Last name is required" }),
email: z
.string({
required_error: "Email is required",
})
.email({ message: "Invalid email address" }),
phoneNumber: z
.string()
.regex(/^[0-9]+$/, { message: "Invalid phone number form" }),
password: z
.string({ required_error: "Password is required" })
.min(8, { message: "Should not be less than 8 characters" })
.max(16, { message: "Should not be no more than 16 characters" }),
confirmPassword: z
.string()
.nonempty({ message: "You need to confirm password" }),
role: z.string().min(1, { message: "Please choose your role" }),
addressFirst: z.string().optional(),
addressSecond: z.string().optional(),
city: z.string().optional(),
country: z.string().optional(),
zipCode: z.string().optional(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Password do not match",
});
export type SignupInput = z.infer<typeof signupSchema>;

Creating Schema & Type

I created schema variable called signupSchema. As data will be in object type, I used .object() (opens in a new tab) and objectified them.

firstName & lastName

Basically I made first name and last name as required value. Therefore, I used .nonempty() (opens in a new tab) for marking them as required, and showed error message if they didn't pass the validation.

email

With using .email() (opens in a new tab) within .string(), I could easily check whether email address is in valid format or not.

phoneNumber

At first I thought phoneNumber should be a number but ended up saving it as a string type for unifying types of signup inputs.

So I used .regex() to check whether it contains other strings (including space) other than numbers.

password

I used .min() and .max() to set minimum and maximum characters of password, respectively.

confirmPassword

With using .refine() (opens in a new tab), I checked whether confirmPassword is as same as password, and if not I made it threw an error message.

role

As I let users to choose their role in their frontend with using <select> and <option> tags, I set .min() to be 1 so that it could be the required value (Default value is empty string).

Fields For Address

I set all fields for addresss as optional with using .optional() (opens in a new tab).

Type Export

I created type of SignupInput that could be matched up with signupSchema.


import { z } from "zod";
export const signupSchema = z.object();
export type SignupInput = z.infer<typeof signupSchema>;

Making Schema File (Login)