React and express.js form validations with Zod
Tags: react, webdev, javascript, ux
Published: 2022-12-05 02:49:42

Whether you are a front-end developer or a back-end developer, you probably have faced difficulties with validating forms, sometimes you are writing too many `if` `else` statements that will make your code bloated and hard to maintain.

The best way to handle form validations throughout your code is definitely not by writing lines and lines of control flow statements, but to use a library that does all of that for us.

There are many libraries out there that you can use, but in this blog post, I will be using Zod along with React.js to do all of the form validations from the front end and show the errors back to the user.

Also, we will be using Zod again to validate the same form from the back which we will be writing using Node.js and Express.js.

## What is Zod?

Zod is a TypeScript-first schema declaration and validation library. Zod can be used for any validation, from a simple string type to a very complex nested object.

The best thing about Zod is that it is very user-friendly and easy to use, it helps avoid duplicate declarations.

You declare the validator and Zod will infer the static type in TypeScript.

For example, you have a simple schema written in Zod

```tsx
const User = z.object({
  name: z.string(),
  email: z.string().email(),
})
```

You can get the TypeScript type by writing the following code

```tsx
type UserType = z.infer<typeof User>
```

It will generate the following TypeScript type

```tsx
type UserType = {
    name: string;
    email: string;
}
```

Some other great aspects of Zod:

- No dependencies
- Works in Node.js and all modern browsers
- Tiny: 8kb minified + zipped
- Immutable: methods (i.e. `.optional()`) return a new instance
- Concise, chainable interface
- Functional approach: **[parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)**
- Works with plain JavaScript too! You don't need to use TypeScript.

So let’s get started with some form validations with Zod.

## Getting started

For the front end, we will be creating a simple React application and have a form in it, we will be validating the form by using Zod and showing the errors back to the user if there are any.

## Setting up a React application

Let’s create a simple React application using Vite.

**Vite** is **a rapid development tool for modern web projects**. It focuses on speed and performance by improving the development experience. Vite uses native browser ES imports to enable support for modern browsers without a build process.

Run the following command to create a Vite project

```bash
yarn create vite
```

After you run the command, it will ask you to name the project. 

I will name it “react-zod”.

![Vite create provide project name](https://i.ibb.co/M5kk4zt/image.png)

Vite create provide project name

After that select your preferred front-end framework

![Vite React](https://i.ibb.co/897VXY7/Untitled.png)

Vite React

After that select your programming language, and I will choose TypeScript.

Zod does not require you to use TypeScript, so if you want to go ahead with JavaScript it is totally fine.

![Vite.js React.js TypeScript project](https://i.ibb.co/L6MMd16/Untitled.png)

Vite.js React.js TypeScript project

After that, your application will be created and you can `cd` into it and install the dependencies by running `yarn` in your terminal.

I will also install TailwindCSS with React, which makes writing CSS code much easier, follow [this simple tutorial to install TailwindCSS in your React project](https://tailwindcss.com/docs/guides/create-react-app).

I will create a sample form that contains some different data types just for the tutorial.

This is our first and only page of the React application, with this form we will test and validate all of our inputs.

```tsx
import { ChangeEvent, FormEventHandler, useState } from "react"

function App() {
  const [data, setData] = useState<any>({})

  const onSubmit: FormEventHandler<HTMLFormElement> = (e) => {
    e.preventDefault()
  }

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target

    if (name === "isAdmin") {
      setData((prev: any) => ({ ...prev, [name]: !prev.isAdmin }))
    } else {
      setData((prev: any) => ({ ...prev, [name]: value }))
    }
  }

  return (
    <div className="w-full max-w-5xl p-5 mx-auto">
      <h1 className="text-center text-3xl font-bold">
        React Form validation with ZOD
      </h1>

      <form
        onSubmit={onSubmit}
        className="mt-10 max-w-sm mx-auto flex flex-col gap-2"
      >
        <div>
          <label className="mb-1">Name</label>
          <input
            className="px-5 py-2 outline-none border border-gray-300 w-full rounded-md"
            placeholder="Name"
            type="text"
            name="name"
            value={data.name}
            onChange={onChange}
          />
        </div>
        <div>
          <label className="mb-1">Email Address</label>
          <input
            className="px-5 py-2 outline-none border border-gray-300 w-full rounded-md"
            placeholder="Email Address"
            type="text"
            name="email"
            value={data.email}
            onChange={onChange}
          />
        </div>
        <div>
          <label className="mb-1">Password</label>
          <input
            className="px-5 py-2 outline-none border border-gray-300 w-full rounded-md"
            placeholder="Password"
            type="password"
            name="password"
            value={data.password}
            onChange={onChange}
          />
        </div>
        <div>
          <label className="mb-1">Percentage</label>
          <input
            className="px-5 py-2 outline-none border border-gray-300 w-full rounded-md"
            placeholder="Percentage"
            type="text"
            name="percentage"
            value={data.percentage}
            onChange={(e) => {
              const value = Number(e.target.value)
              setData((p: any) => ({ ...p, percentage: value }))
            }}
          />
        </div>
        <div>
          <label className="mb-1">Is Admin</label>
          <input
            className="px-5 py-2 outline-none border border-gray-300 w-full rounded-md"
            placeholder="Boolean"
            type="checkbox"
            name="isAdmin"
            checked={data.isAdmin}
            onChange={onChange}
          />
        </div>

        <button
          type="submit"
          className="py-2 px-5 rounded-lg border hover:bg-gray-50"
        >
          Submit
        </button>
      </form>
    </div>
  )
}

export default App
```

The code above is a simple React component that includes a simple form inside of it which has a few fields that are `name`, `email`, `password`, `percentage`, `isAdmin` each with a different data type.

The goal is to validate any data that will be filled by any user and only submit the form when all of the fields pass all of the form validations that will be taken care of by Zod.

We have a `onChange` function that will be triggered whenever each input has a change in its value

```tsx
  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target

    if (name === "isAdmin") {
      setData((prev: any) => ({ ...prev, [name]: !prev.isAdmin }))
    } else {
      setData((prev: any) => ({ ...prev, [name]: value }))
    }
  }
```

We are also checking if the name is equal to `isAdmin` in that case we are changing the state to the opposite value of the previous value to get a Boolean value.

The `onSumbit` button is an empty function which only has a simple function inside of it which is the `e.preventDefault()` which will prevent the page to reload. We will come back to this function later to handle the submit form action.

This is how the application looks like

![React from with zod](https://i.ibb.co/Vwkm5R9/Untitled.png)

## Installing Zod

Install Zod by running the following command in the terminal

```bash
yarn add zod
```

**Important**: If you are using TypeScript, it is required that you have TypeScript version `4.1` or above installed on your machine, if you haven’t make sure you install the latest version by running the following command

```bash
yarn global add typescript@latest
```

Also it is important to set your `strict` value as `true` in your `tsconfig.json` file.

Now it’s time to create a schema using Zod.

## Creating a schema with Zod

First, let's create a folder and store all of our schemas inside of it.

Create a folder to store all of your schemas inside of your `src` directory, I will name the folder `schemas`

Create a schema type for the form above inside of that folder, `src/schemas/form.ts`

This is how the folder structure looks like

![Vite folder structure](https://i.ibb.co/FqKpj6F/Untitled.png)

Now we can use `zod` by importing it.

```tsx
import { z } from "zod"
```

## Creating our Form object with zod

```tsx
import { z } from "zod"

export const Form = z.object({
  name: z.string(),
  email: z.string().email(),
  password: z.string(),
  percentage: z.number(),
  isAdmin: z.boolean(),
})
```

As you can see, we are using the `object` property of `zod` to tell it that our `Form` object has some specific properties with specific types.

name: `z.string()` makes sure that name is always string

email: `z.email()` makes sure that the email field has a valid e-mail address.

percentage: `z.number()` makes sure that the percentage is a valid number.

isAdmin: `z.boolean()` makes sure that the field contains a boolean.

But this is not all, there are so many other things we can do with `zod` like making sure a certain property has a specific length, or a number is between a specific range.

We can also add custom error messages when a particular field fails validation, for example, if we want the `name` to have a minimum length of 2 and a maximum length of 32, we can do something like this

```tsx
const name = z
  .string()
  .min(2, "Name must be at least 2 characters")
  .max(32, "Name must be at most 32 characters")
```

Let’s make our form much better by adding more validations to it.

```tsx
import { z } from "zod"

export const Form = z.object({
  name: z
    .string({ description: "Your name" })
    .min(2, "Name must be at least 2 characters")
    .max(32, "Name must be at most 32 characters")
    .optional(),
  email: z.string().email(),
  password: z
    .string()
    .min(8, "Password must be at least 8 characters")
    .max(64, "Password must be at most 64 characters"),
  percentage: z
    .number()
    .min(0, "Percentage must be at least 0")
    .max(100, "Percentage must be at most 100"),
  isAdmin: z.boolean().default(false),
})
```

By default, all of the values are expected to be `required`, meaning they should be defined and have a value of their specific type when any data is parsed to the schema. We are using the `optional()` method on the `name` property to make it an optional property.

### Creating TypeScript types from a Zod schema

We can create a TypeScript type directly from our Zod schema just by using the `infer` method.

```tsx
export type FormType = z.infer<typeof Form>
```

This will create the following TypeScript type

```tsx
type FormType = {
    name?: string | undefined;
    email: string;
    password: string;
    percentage: number;
    isAdmin: boolean;
}
```

As you can see, the `name` property is optional in the TypeScript type as well.

### Validate the React form with Zod

Now that our schema is ready to be used, let’s write a function to validate the React form by using Zod.

Go to your `App.tsx` file and import the `Form` schema that we just created.

```tsx
import { Form } from "./schemas/form"
```

Create a function inside of the `App` component with the name `validate` to handle the form validation, here is how the `validate` function looks like

```tsx
const validate = () => {
  const formData = Form.safeParse(data)

  if (!formData.success) {
    const { issues } = formData.error

    setErrors(issues)
  } else {
    setErrors([])
  }
}
```

In this function, we are using the `safeParse` method on the `Form` object. which will return a property with the name `success` which is a `boolean` if the value is `true` it means that the validation was not successful otherwise it is successful.

After the function creation, we can easily call it whenever the form is submitted

```tsx
 const onSubmit: FormEventHandler<HTMLFormElement> = (e) => {
    e.preventDefault()

    validate()
  }
```

### Error handling with Zod

When the validation is not successful or the `success` value equals `false` then another property will be returned from the `safeParse` method which is the `error` object which also has another property on it with the name `issues` which is exactly what we need.

The `issues` property is an array of objects with the following schema

```tsx
{
  code: "invalid_type",
  expected: "string",
  message: "Required",
  path: ["email"],
  received: "undefined",
}
```

What we need in this object is the `message` property, we can easily access this `message` property and use it to show it back to the user so that they will understand why the validation failed.

This `message` property can also be easily configured when the Zod schema is initialized, for example,

```tsx
const User = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'), // will show this error if "name" property has a length of less than 2 characters
  email: z.string().email(),
})
```

Back to our `validate` function, we are also running a function with the name `setErrors` which updates the state `errors` which were defined above by using a simple `useState` hook.

```tsx
const [errors, setErrors] = useState([])
```

If you want to make this type-safe, you can use the `ZodIssue` type which comes with Zod which can be imported easily

```tsx
import { ZodIssue } from "zod"
```

Updating the state

```tsx
const [errors, setErrors] = useState<ZodIssue[]>([])
```

### Showing the error back to the user

Now that we have written our `validate` function, if there is any error when the user submits the form, it will update the `errors` state with the new errors.

We can use this state to show it back to the user.

I will create a new function with the name `getError` which will get one argument `path` and return the error message in the `errors` state.

```tsx
const getError = (path: string) => {
    const error = errors.find((error) => error.path === path)

    return error ? (
      <small className="text-red-500">{error?.message}</small>
    ) : null
  }
```

As you can see we are returning a `JSX` element `<small>` with a red colour.

We can run this function under every input element in our Form component.

This is how our Form component looks like

```tsx
<form
  onSubmit={onSubmit}
  className="mt-10 max-w-sm mx-auto flex flex-col gap-2"
>
  <div>
    <label className="mb-1">Name</label>
    <input
      className="px-5 py-2 outline-none border border-gray-300 w-full rounded-md"
      placeholder="Name"
      type="text"
      name="name"
      value={data.name}
      onChange={onChange}
    />

    {getError("name")}
  </div>
  <div>
    <label className="mb-1">Email Address</label>
    <input
      className="px-5 py-2 outline-none border border-gray-300 w-full rounded-md"
      placeholder="Email Address"
      type="text"
      name="email"
      value={data.email}
      onChange={onChange}
    />
    {getError("email")}
  </div>
  <div>
    <label className="mb-1">Password</label>
    <input
      className="px-5 py-2 outline-none border border-gray-300 w-full rounded-md"
      placeholder="Password"
      type="password"
      name="password"
      value={data.password}
      onChange={onChange}
    />
    {getError("password")}
  </div>
  <div>
    <label className="mb-1">Percentage</label>
    <input
      className="px-5 py-2 outline-none border border-gray-300 w-full rounded-md"
      placeholder="Percentage"
      type="text"
      name="percentage"
      value={data.percentage}
      onChange={(e) => {
        const value = Number(e.target.value)
        setData((p: any) => ({ ...p, percentage: value }))
      }}
    />
    {getError("percentage")}
  </div>
  <div>
    <label className="mb-1">Is Admin</label>
    <input
      className="px-5 py-2 outline-none border border-gray-300 w-full rounded-md"
      placeholder="Boolean"
      type="checkbox"
      name="isAdmin"
      checked={data.isAdmin}
      onChange={onChange}
    />
    {getError("isAdmin")}
  </div>

  <button
    type="submit"
    className="py-2 px-5 rounded-lg border hover:bg-gray-50"
  >
    Submit
  </button>
</form>
```

### Trying it out

Now it is time to try out our application and see if it works as expected.

First, let's submit the form with empty values.

![zod validating empty field](https://i.ibb.co/QbnXM8q/Untitled.png)

As you can see we get `required` errors for objects that didn’t have the `optional()` method run on.

Let’s add data to the fields

![Zod validating react form](https://i.ibb.co/P1ZwvJK/Untitled.png)

Now we no longer get the `required` error messages, but we get other validation messages that we have defined above.

If we fill out the form without any mistakes, we no longer will see the validation errors.

![Zod successful validation with React](https://i.ibb.co/YjK5fm6/Untitled.png)

Congratulations, now your form is validated with as little effort as possible, thanks to Zod there is no longer the need to write lines and lines of `if` and `else` cases and other control flow statements to validate your forms.

## Validating a form in Node.js with Zod

let’s create a simple Node.js backend by using `express.js` to validate the same form that we have just created, but this time from the backend and sending back the proper error messages to the client.

This is how our simple node.js server code looks like

```jsx
const express = require('express')
const app = express()

app.use(express.json())

app.post('/form/submit', (req, res) => {})

const PORT = process.env.PORT || 3000

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`)
})
```

Let’s update the `POST` route with the proper code to validate the same data in the form that we created in the front end.

### Creating a Zod Schema

First things first, let’s create the Zod schema for the form validations.

```jsx
const Form = z.object({
  name: z
    .string()
    .min(2, 'Name must be at least 2 characters')
    .max(32, 'Name must be at most 32 characters')
    .optional(),
  email: z.string({ required_error: 'Email is required' }).email(),
  password: z
    .string({ required_error: 'Password is required' })
    .min(8, 'Password must be at least 8 characters')
    .max(64, 'Password must be at most 64 characters'),
  me: z.string({ required_error: 'You must provide me' }),
  percentage: z
    .number({ required_error: 'Percentage is required' })
    .min(0, 'Percentage must be at least 0')
    .max(100, 'Percentage must be at most 100'),
  isAdmin: z.boolean().default(false)
})
```

This one is a bit different, when we are initializing a type of a property like `z.string()` we are also passing in the options and setting the `required_error` property, this way were are able to overwrite the default required error message.

### Creating and validating the route with express and Zod

Now that our schema is ready to use, let’s create a `POST` route with express and validate the input data with Zod.

```jsx
app.post('/form/submit', (req, res) => {
  try {
    const body = Form.safeParse(req.body)

    if (!body.success) {
      res.status(400).json({ message: body.error.issues[0].message })
      return
    }

    // Do something with the data

    res.json({
      message: 'Form submitted successfully'
    })
  } catch (error) {
    res.json({
      message: error.message
    })
  }
})
```

Of course, you can send a different response back to the client if there is an issue in validating the form, but for this tutorial, I will be sending back the first issue message.

Let’s test our route with various data and see if it works as expected

First test

```jsx
// request
{
  "name": "John",
  "email": "test", 
  "password": "123456",
  "isAdmin": "true"
}

// response
{
  "message": "Invalid email"
}
```

Second test

```jsx
// request
{
  "name": "John",
  "email": "test@gmail.com", 
  "password": "123456",
  "isAdmin": "true"
}

// response
{
  "message": "Password must be at least 8 characters"
}
```

Third test

```jsx
// request

{
  "name": "John",
  "email": "test@gmail.com", 
  "password": "12345645",
  "isAdmin": "true"
}

// response
{
  "message": "Percentage is required"
}
```

Fourth test

```jsx
// request
{
  "name": "John",
  "email": "test@gmail.com", 
  "password": "12345645",
  "isAdmin": "true", 
  "percentage": 3
}

// response
{
  "message": "Expected boolean, received string"
}
```

Final test

```jsx
// request
{
  "name": "John",
  "email": "test@gmail.com", 
  "password": "12345645",
  "isAdmin": true, 
  "percentage": 3
}

// response
{
  "message": "Form submitted successfully"
}
```

Congratulations, now your form is validated strongly from the back end as well, without having to do too many configurations and writing endless lines of code which will make your code extremely bloated and making it more difficult to maintain.

Since Zod works perfectly well with TypeScript, it will make your code type-safe leaving no space for unexpected errors and bugs.

Thanks for reading this blog post, if you enjoyed the blog, make sure you share it with other developers so that they can benefit from it as well.

If you have any notes, please let me know from the comments.