Zod - TypeScript-first schema declaration and validation library #tips

Zod - TypeScript-first schema declaration and validation library #tips


Schema Validation with Zod and Express.js

Overview

In the past, I have done articles on what is [Zod](https://zod.dev/), and how to use Zod to declare a validator once and compose simpler types into complex data structures.

Today’s example

Today, I will write an article where we will create middleware to validate a specific route’s schema.

The idea is quite simple, let’s create a middleware that will receive a schema as a single argument and then validate it.

Project setup

As a first step, create a project directory and navigate into it:

mkdir zod-expressjs-sample
cd zod-expressjs-sample

Next, initialize an Express.js project and add the necessary dependencies:

npm init -y
npm install express zod

Now let’s update some configs in our package.json file.

{
 …
 "type": "module",
 "main": "index.mjs",
 …
}

Let’s code

And now let’s create a simple API:

// Path: index.mjs
import express from 'express'
import cors from 'cors'

const app = express()

const port = 3000

app.use(cors())
app.use(express.json({ limit: '50mb' }))

app.get('/', (req, res) => {
 res.json({ message: 'Hello World!' })
})
app.listen(port, () => {
 console.log(`Example app listening on port ${port}`)
})

For the API to be initialized on port 3000 just run the following command:

node index.mjs

Now we can start working with Zod, and first, let’s define our schema. In this example, we will only validate the response body. And let’s hope the body contains two properties, the fullName and the email. This way:

// Path: index.mjs
import express from 'express'
import cors from 'cors'
import zod from 'zod'

const app = express()

const port = 3000

app.use(cors())

app.use(express.json({ limit: '50mb' }))

const dataSchema = zod.object({
 body: zod.object({
 fullName: zod.string({
 required_error: 'Full name is required',
 }),
 email: zod
 .string({
 required_error: 'Email is required',
 })
 .email('Not a valid email'),
 }),
})

// …

Now we can create our middleware. When the user calls our middleware, validate and receive schema validation in the arguments.

Finally, if it is properly filled in, we will go to the controller.

Otherwise, we will send an error message to the user.

// Path: index.mjs

// …

const validate = (schema) => async (req, res, next) => {
  try {
    await schema.parseAsync({
      body: req.body,
      query: req.query,
      params: req.params,
    })
    return next()
  } catch (error) {
    return res.status(400).json(error)
  }
}

// …

Finally, we are going to create a route with the HTTP verb of POST type, which we will use our middleware to perform the validation of the body, and if successful, we will send the data submitted by the user.

// Path: index.mjs

import express from 'express'
import cors from 'cors'
import zod from 'zod'

const app = express()

const port = 3000

app.use(cors())
app.use(express.json({ limit: '50mb' }))

const dataSchema = zod.object({
  body: zod.object({
    fullName: zod.string({
      required_error: 'Full name is required',
    }),
    email: zod
      .string({
        required_error: 'Email is required',
      })
      .email('Not a valid email'),
  }),
})

const validate = (schema) => async (req, res, next) => {
  try {
    await schema.parseAsync({
      body: req.body,
      query: req.query,
      params: req.params,
    })
    return next()
  } catch (error) {
    return res.status(400).json(error)
  }
}

app.post('/create', validate(dataSchema), (req, res) => {
  return res.json({ ...req.body })
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

Testing

1️⃣ Case 1: Blank object

{}

We get a response with the status code 400 Bad Request:

{
    "issues": [
        {
            "code": "invalid_type",
            "expected": "string",
            "received": "undefined",
            "path": [
                "body",
                "fullName"
            ],
            "message": "Full name is required"
        },
        {
            "code": "invalid_type",
            "expected": "string",
            "received": "undefined",
            "path": [
                "body",
                "email"
            ],
            "message": "Email is required"
        }
    ],
    "name": "ZodError"
}

2️⃣ Case 2: fullName and email are empty

{
    "fullName": "",
    "email": ""
}

We get a response with status code 400 Bad Request:

{
    "issues": [
        {
            "validation": "email",
            "code": "invalid_string",
            "message": "Not a valid email",
            "path": [
                "body",
                "email"
            ]
        }
    ],
    "name": "ZodError"
}

3️⃣ Case 3: valid fullName, invalid email format

{
    "fullName": "Nhan Nguyen",
    "email": "sample@gmail"
}

We also get a response with the status code 400 Bad Request:

{
    "issues": [
        {
            "validation": "email",
            "code": "invalid_string",
            "message": "Not a valid email",
            "path": [
                "body",
                "email"
            ]
        }
    ],
    "name": "ZodError"
}

4️⃣ Case 4: both fullName and email are valid

{
    "fullName": "Nhan Nguyen",
    "email": "sample@gmail.com"
}

We get a response with status code 200 OK:

{
    "fullName": "Nhan Nguyen",
    "email": "sample@gmail.com"
}

Conclusion

As always, I hope you found it interesting. If you notice any errors in this article, please mention them in the comments. 🧑🏻‍💻

Hope you have a great day! 🤗


I hope you found it useful. Thanks for reading. 🙏

Let’s get connected! You can find me on: