Skip to content

Validate incoming requests using Zod validation in Typescript

What's Zod validation?

Zod is a schema validation library, in other words it helps us to create schemas and compare that specific objects are valid using the schema as rules. We can take advantage of this to validate incoming requests payloads

Creating new project

First we need to create a new Typescript node project

mkdir zod-validation-typescript
cd zod-validation-typescript
npm init -y

This is a list of all the packages needed and the reason why we will install them:

"express" (npm install express): Simplifies the creation of RESTful APIs
using Node
``` mechanism that only allow access to our resources for specified domains
"zod" (npm install zod): Typescript-first Schema declaration and validation
using Node
"dotenv" (npm install dotenv): Loads environment variables from a .env
"typescript" (npm install -D typescript): TypeScript adds optional types to JavaScript that support tools for large-scale JavaScript applications for any browser
"ts-node" (npm install -D ts-node): Transforms TypeScript into JavaScript
"@types/express" (npm install --save @types/express): Types definition for express
npm i express zod dotenv
npm i --save-dev typescript ts-node @types/express

Next we will create a tsconfig.json file

npx tsc --init

Change the tsconfig.json file that was created

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "declaration": true,
    "sourceMap": true,
    "outDir": "dist",
    "rootDir": "./",
    "strict": true,
    "esModuleInterop": true
  }
}

Now change the package.json file

{
  "name": "zod-validation-typescript",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "start": "ts-node index.ts",
    "build": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/express": "^4.17.18",
    "ts-node": "^10.9.1",
    "typescript": "^5.2.2"
  }
}

Creating server starting point and routes

Next we are going to create an index.ts file in the base of the project, we will add some routes that need validation

// ./index.ts

import 'dotenv/config'

import express, { Application, Request, Response } from "express";

import {validateRequest} from "./src/middlewares/validator";
import {bodyParamsValidator, queryParamsValidator, queryStringValidator} from "./src/validators";

const port = 3015;
// New express application instance
const app: Application = express();

// Allow json in body params
app.use(express.json({ limit: "250mb" }))

/**
 * Route using query string
 */
app.get("/query-string", [validateRequest(queryStringValidator)], (req: Request, res: Response) => {
    /**
     * Get variables from query string
     */
    const { name, lastName, age, status  } = req.query

    res.status(200).send({ name, lastName, age, status })
})

/**
 * Route using body params
 */
app.post("/body-params", [validateRequest(bodyParamsValidator)], (req: Request, res: Response) => {
    /**
     * Get variables from body params
     */
    const { email, password  } = req.body

    res.status(200).send({ email, password })
})

/**
 * Route using query params
 */
app.get("/query-params/:userId/:bookId", [validateRequest(queryParamsValidator)], (req: Request, res: Response) => {
    /**
     * Get variables from query params
     */
    const { userId, bookId  } = req.params

    res.status(200).send({ userId, bookId })
})


/**
 * Begin listening on port selected
 */
app.listen(port, () => {
    console.log(`Server is running on port ${port}...`);
});

Creating validators

Now we are going to create a new directory on the base of the project called src, inside of this new directory create another one called validators, inside this new folder create file called index.ts

// ./src/validators/index.ts
import { z } from "zod";

export enum UserStatus {
    ACTIVE = "active",
    DELETED = "deleted"
}

export const queryStringValidator = z.object({
    query: z.object({
        // Name is required and must have 4 or more characters
        name: z.string({
            required_error: "Name is required",
        }).min(4, "Name must be bigger than 4 characters"),
        // Last name is optional
        lastName: z.string().optional(),
        // Age is required, we expect a number but, because we are receiving values from query string all will be stirngs
        age: z.string({
            required_error: "Age is required",
        }),
        // Status is required and must one of the value defined in the enum
        status: z.nativeEnum(UserStatus, { required_error: "Status is required", invalid_type_error: "Status is not valid" }),
    }),
});

export const bodyParamsValidator = z.object({
    body: z.object({
        // Email is required and must have email format
        email: z.string({
            required_error: "Email is required",
        }).email("Email is not valid"),
        // Password is required and must have 8 or more characters
        password: z.string({ required_error: "Password is required" }).min(8, "Password must be minimum 8 characters long")
    }),
});

export const queryParamsValidator = z.object({
    params: z.object({
        // User id is required
        userId: z.string({
            required_error: "User id is required",
        }),
        // Book id is required
        bookId: z.string({
            required_error: "Book id is required",
        }),
    }),
});

Creating a validation middleware

Now we are going to create a middleware that will validate that the requests have all the fields needed, before we hit the routes logic. Create a new directory inside of src called middlewares, inside of this new folder create a file called validator.ts

// ./src/middlewares/validator.ts
import {AnyZodObject, ZodError} from "zod";
import { Request, Response, NextFunction } from "express";

/**
 * desc validates that the request query, params and body are valid
 * @param validator the Zod validation that will be used to validate the request
 */
export const validateRequest = (validator: AnyZodObject) => async (req: Request, res: Response, next: NextFunction) => {
    try {
        // We use parse to validate the request is valid
        await validator.parseAsync({
            body: req.body,
            query: req.query,
            params: req.params,
        });

        // Validation was successfully continue
        next();
    } catch (error) {
        // If error is instance of ZodError then return error to client to show it to user
        if (error instanceof ZodError) {
            return res.status(400).send({ msg: error.issues[0].message } );
        }

        // If error is not from zod then return generic error message
        return res.status(500).send("Error making request, contact support" );
    }
};

Now start the server, and try to access any of the specified routes you will get the error messages if the request is not valid

Running project

npm run start

GitHub repository

In this repository https://github.com/obravocedillo/ZodValidation you can find the code.

Postman documentation

In this link https://documenter.getpostman.com/view/11491178/2s9YJhwKV2 you can find the Postman documentation with all the links and required params.