Skip to main content

Command Palette

Search for a command to run...

How To Validate Your Express Payloads With Zod?

Published
How To Validate Your Express Payloads With Zod?

You don’t know what will come in your payloads. When working with req.body, can you really be sure it contains all the fields you expect? No. You are completely unaware of what is coming in the payload.

This is a common problem that almost every Express.js developer faces at some point in their journey, especially beginners. You might be using multiple if-else conditions to protect your API from invalid payloads, but those if-else blocks quietly make your code messy, hard to read, and difficult to maintain.

Developers Can’t Predict The Future

It’s true. We are not astrologers. We cannot predict what data a user will send or what a mental frontend developer might pass to your API. You can’t predict it, but you can protect against it. There are zillions of packages available on npm, from express-validator to yup. Let’s talk about Zod.

Developers Can Protect The Future

Protection means validating your payload schema. Is the incoming payload valid for this endpoint or not? If it isn’t, you should return a clean and meaningful 400 error to that frontend guy.

This can be done very easily using middleware. We’ll create an Express middleware that validates payloads for different API endpoints and prevents invalid data from reaching your business logic.

What can you validate? Well!, Well…, the list is long. I don’t know what you’re building or what you’ll build in the future. I don’t know your project requirements. So instead, I’ll show you a simple Express application protected with Zod.

Installing Zod

We’re lucky this time. We have multiple package managers, so let’s look at how to install Zod using some of them.

Installing Zod With NPM

npm install zod

You can install Zod using npm with a single command. This is the simplest way. You can install Express the same way, though I assume you’re not that beginner.

Installing Zod With PNPM

pnpm add zod

Installation of zod with pnpm is simple just an add command instead of install command.

Installing Zod With My Breakfast

bun add zod

Don’t take me wrong, I actually eat bun in my breakfast.

Let’s Set Up Your Express App

I know you can do this your own, But I want to make this blog long so I have more content to write. Here is the process of making an Express App.

Index File

// server.js

import express from "express";
import userRouter from "./routes/user.route.js";

const app = express();
const port = 3000;

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use("/api/auth", userRouter);

app.listen(port, () => {
  console.log(`Server is running on port http://localhost:${port}/health`);
});

Nothing! Just importing express and userRouter, Initializing express instance then some middlewares and listing on port 3000.

Controller To Control User Registration Logic

// ./controllers/user.controller.js

export const registerUser = async (req, res) => {
  try {
    const { email, password, username } = req.body;

    /* User Registration Logic */

    res
      .status(201)
      .json({
        message: "User registered successfully",
        user: { id: "123", email, username },
      });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

In the controller, we extract email, password, and username from req.body. This is where your actual user registration logic would live, such as hashing passwords and saving users to a database.

We return a 201 status code on success. As a good boy the logic is wrapped in a try-catch block because, we can’t predict the future.

Creating Zod Middleware To Validate Payload

// ./middlewares/zod.middleware.js

export default (zodSchema) => (req, res, next) => {
  const result = zodSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ error: result.error.flatten() });
  }

  req.body = result.data;
  next();
};

Oh… Sh!t, What the hell is this? No its easy, First thing first we are exporting a function which takes a zodSchema as it’s only parameter and it returns another function which takes three arguments req, res and next. The returning function which takes those three parameters is our actual zod middleware, To understand this code more effectively consider the below code.

export default (zodSchema) => {
  return (req, res, next) => {
    const result = zodSchema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({ error: result.error.flatten() });
    }

    req.body = result.data;
    next();
  };
}

Inside the middleware, we call safeParse on the schema and pass req.body to it. The result tells us whether validation succeeded or failed. If validation fails, we immediately return a 400 response with detailed error information. If it succeeds, we replace req.body with the validated and parsed data and call next().Creating And Understanding Zod Schema

This pattern allows us to reuse the same middleware for different routes with different schemas.

Just like you define schemas using Mongoose for MongoDB, Zod lets you define schemas for almost anything. Payload validation is just one use case. Many developers even use Zod when building AI agents and workflows because it enforces a fixed structure that LLM can reliably understand.

Zod provides pretty good methods like .string() .email() .url() .boolean() .min() .max() .describe() and more.

Let’s create a schema for students, A student must have a name which will be a string, rollNumber could be a number. Keep it simple with just two field, each student is an Object and we have more then one student so we need an array of this student’s objects.

import { z } from "zod";

const studentsSchema = z.array(
    z.object({
        name: z.string().describe("Students name in title case"),
        rollNumber: z.number().min(1).describe("Students roll number, should be greater than 1"),
    })
)

Writing Our Own Zod Schema For Our Payload

// ./schema/userResgistration.schema.js

import { z } from "zod";

export default z.object({
  email: z.email(),
  password: z.string().min(8).max(100),
  username: z.string().min(2).max(100),
});

It describes that our payload is an Object which has three properties email, password and username. Email should be a valid email address, password must be a string which can be have minimum of 8 characters and maximum of 100 characters, Also the username is string but the limit of 2 and 100.

Express Router

This isn’t done yet, We will create our post route on /register, Using express’s Router.

// ./routes/user.route.js

import { Router } from "express";
import { registerUser } from "../controllers/user.controller.js";
import userResitrationSchema from "../schema/userResitration.schema.js";
import zodMiddleware from "../middlewares/zod.middleware.js";

const router = Router();

router.post("/register", zodMiddleware(userResitrationSchema), registerUser);

export default router;

This is mostly simple to understand for a backend express developer, we are importing Router from express, our registerUser controller, The zod schema we defined in ./schema/userResitration.schema.js and our zodMiddleware.

Creating instance of Router and registering our zodMiddleware and registration controller registerUser to /register route.

The most important LoC is:

router.post("/register", zodMiddleware(userResitrationSchema), registerUser);

We are passing three arguments, Our endpoint, zod middleware and our controller. The zod middleware looks something different, Yes! Because of how we define it in the zod.middleware.js file. We are zodMiddleware and passing userResitrationSchema as zodSchema, which returns a middleware.

That’s it the code is runnable and you can see whole code here.

Output When User Sends An Invalid Payload

Payload

{
  "password": "12348",
  "username": "Thisismyusername"
}

Here the email field is missing and password is less then 8 characters, So our backend should give 400 error.

API Result

{
  "error": {
    "formErrors": [],
    "fieldErrors": {
      "email": [
        "Invalid input: expected string, received undefined"
      ],
      "password": [
        "Too small: expected string to have >=8 characters"
      ]
    }
  }
}

Our Future Is Protected Now

I don’t know how much you understood from this article, and I’ll admit I’m not the best writer. But if this is your first time reading about Zod in this way and it helped you, feel free to connect with me on GitHub, Instagram, X, or LinkedIn.

All the code is available in the repository.