Client & server-side validation with Zod.
Tags: webdev, cloudcomputing, tutorial, typescript
Published: 2023-07-06 16:58:15

Validating the data we receive from various sources is crucial. It's a common challenge faced when dealing with user input, external APIs, or data read from databases.

In the JavaScript world, we have several tools to help with data validation, but one that stands out is **[Zod](https://zod.dev/)** - a library for creating, manipulating, and validating schemas.

In this blog, we'll dive into the details of creating schemas using Zod. By the end, you will be able to confidently use Zod to create and validate your data models. 

_As a side note, I spent a good portion of my career in the fintech space with onboarding applications - aligning validation was always finicky._


Let's get started!

## What is Zod?

[Zod](https://zod.dev/) is a JavaScript library that allows developers to build schemas for their data. It is simple yet powerful, providing a fluent API for constructing schemas and validating data. With Zod, you can create complex, nested object schemas with deep type inference, default values, transformations, and more.

## Setting Up

Before we start creating schemas, you'll need to install Zod using npm with the following command:

```bash
npm install zod
```

## Creating a Basic Schema

Let's start by creating a basic schema for a user. In this schema, a user will have a name, email, and age.

```javascript
import { z } from 'zod';

const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(0),
});
```

Here we define an object schema, `userSchema`, where `name` and `email` are string fields, with `email` being validated with the built-in email validator. The `age` is a number field with a minimum value of 0.

## Validating Data with the Schema

Now, let's use our schema to validate some data.

```javascript
const userData = {
  name: "John Doe",
  email: "john.doe@example.com",
  age: 30,
};

const result = userSchema.safeParse(userData);

if (!result.success) {
  console.log(result.error);
} else {
  console.log(result.data);
}
```

`safeParse` is a method provided by Zod that returns an object. If the validation fails, `success` will be false, and the `error` object will contain information about what went wrong. If the validation passes, `success` will be true, and `data` will contain the validated data.

## Advanced Schema

Zod also allows creating advanced schemas with optional fields, default values, and custom validation. Let's create an advanced user schema.

```javascript
const advancedUserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(0),
  website: z.string().optional().url(),
  bio: z.string().default("This user hasn't written their bio yet."),
});
```

In this advanced schema, the `website` field is optional and must be a valid URL. The `bio` field has a default value and will automatically be filled in if the user doesn't provide one.

## Custom Validators

Zod is not limited to predefined validators; you can write your own custom validators using the `.refine()` method.

```javascript
const passwordSchema = z.string().refine(password => password.length >= 8, {
  message: "Password must be at least 8 characters long",
  path: ['password'], // indicates this error is about the password field
});
```

In this schema, the `password` field must be a string that is at least 8 characters long. If it's not, Zod will throw an error with our custom message.

**Client & Server-Side Validation**

So far, we've covered creating basic and advanced schemas using Zod, as well as using custom validators.

Client-side and server-side validation with Zod can be implemented in quite a similar way because both are run on JavaScript-based environments. The only difference would be where and when the validation is performed.

The primary advantage of using Zod in both client and server-side is the DRY (Don't Repeat Yourself) principle. You can define your validation schemas once and use them on both ends. This ensures that your data is consistent and follows the same rules across your entire application.

**Client-Side Validation**

On the client-side, you will typically use Zod validation in response to user interaction. For example, you might validate form data when a user submits a form. Here's an example using React:

```jsx
import { z } from 'zod';
import React, { useState } from 'react';

const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(0),
});

function UserForm() {
  const [formData, setFormData] = useState({ name: '', email: '', age: '' });
  const [errors, setErrors] = useState({});

  const handleSubmit = (event) => {
    event.preventDefault();
    const result = userSchema.safeParse(formData);
    if (result.success) {
      // Proceed with form submission
      console.log(result.data);
    } else {
      // Handle validation errors
      setErrors(result.error.formErrors.fieldErrors);
    }
  };

  // The rest of your form goes here
}
```

In this example, when the form is submitted, we parse the form data using the `userSchema`. If validation is successful, we continue with form submission; otherwise, we show the errors.

**Server-Side Validation**

On the server-side, you will typically use Zod validation in your route handlers, immediately after receiving a request. Here's an example using the [Nitric framework](https://nitric.io):

```javascript
import { api } from "@nitric/sdk";
import { z } from "zod";

const mainApi = api("main");

const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(0),
});

mainApi.get("/user", async (ctx) => {
  const result = userSchema.safeParse(ctx.req.json());
  if (result.success) {
    // Proceed with processing the request
    ctx.res.status = 200;
    ctx.res.json(result.data);
  } else {
    // Handle validation errors
    ctx.res.status = 400;
    ctx.res.json(result.error.formErrors.fieldErrors);
  }
  return ctx;
});
```

In this example, when a POST request is made to the "/user" endpoint, we parse the request body using the `userSchema`. If validation is successful, we continue processing the request; otherwise, we return a 400 status code along with the validation errors.

```bash
curl -X GET -d '{"name":"John","email":"john@example.com","age":"abcd"}' -H 'Content-Type: application/json' http://localhost:4001/user

{"age":["Expected number, received string"]}%     
```

Zod can be used for both client-side and server-side validation in JavaScript applications, providing a consistent validation experience across your application.

Make sure to always validate user data on the server-side, even if you have client-side validation, to ensure data integrity and security.