Request Validation in Express with Joi

While working with APIs, the validation of requests in Node.js and Express is no longer optional. Every day, users send broken, missing, or even malicious data. In this guide, you’ll learn how to validate API requests within Node.js Express by using Joi in a clean and very scalable way that suits real projects. We’ll go through a few simple examples, a login use case, common data types, and then end up with a reusable Express.js request validation middleware pattern. You’ll know exactly how to validate request body in Express.js using Joi and keep your validation rules organised for the long run by the end.

Why Request Validation Matters Before Your Controller Runs

When a request hits your API, it can be:

  • Missing required fields
  • Using improper data types
  • Longer or shorter than allowed
  • Intentionally crafted to break your app or leak data

Request validation checks the shape and content of incoming data before your business logic runs: you define what “valid” looks like, and everything else gets rejected with a clear error.

Why You Should Validate Requests in Node.js and Express

  • Security: Invalid data can result in injection attacks, crashes, or unexpected behavior. With good request validation in Node.js and Express, you reduce that risk dramatically.
  • Better developer experience: You can trust req.body in controllers, when every request follows a schema. That makes debugging and onboarding much easier.
  • Better user experience: Clear, consistent validation errors help front-end teams show a friendly message instead of generic “Something went wrong” alerts.
  • More stable APIs: You avoid random crashes due to undefined properties, wrong types, or missing fields.

Introduction of Joi for Node.js and Express

Joi is a popular validation library for JavaScript and TypeScript. It lets you describe the shape of your data using schemas. Those schemas then validate any object against your rules. Instead of writing a lot of if statements, you write a Joi schema, such as: email must be a valid email, password must be at least 8 characters.

Joi works great with Express and is one of the most common choices for joi validation in Node.js Express projects.

Installing Joi

From your project root, install Joi with npm or yarn:

npm install joi

Validating Common Data Types with Joi

Let us start with a simple Joi schema validation example in Node.js. Imagine a route that creates a user profile.

const express = require('express');
const Joi = require('joi');

const app = express();
app.use(express.json());

const userSchema = Joi.object({
  name: Joi.string().min(3).max(30).required(),
  age: Joi.number().integer().min(18).max(100).required(),
  email: Joi.string().email().required(),
  active: Joi.boolean().default(true),
  tags: Joi.array().items(Joi.string().max(20)).max(5)
});

app.post('/users', async (req, res) => {
  try {
    const value = await userSchema.validateAsync(req.body, {
      abortEarly: false,
      stripUnknown: true
    });

    res.status(201).json({
      message: 'User created',
      data: value
    });
  } catch (err) {
    res.status(400).json({
      message: 'Validation failed',
      errors: err.details.map(d => d.message)
    });
  }
});

app.listen(3000);

It shows how to validate API input in Node.js using Joi. The string() for text, number() for numeric value, boolean() for flags and array() for lists.

You also see abortEarly: false, which collects all validation errors instead of stopping at the first one. stripUnknown: true removes unexpected fields, which helps keep your data clean.

Example: Basic Login Request Validation

Let’s start with building a simple login endpoint. This is a very common real-life case and a great place to start with how to validate request body in Express.js.

Login schema with Joi

const Joi = require('joi');

const loginSchema = Joi.object({
  email: Joi.string().email().required().messages({
    'string.email': 'Please enter a valid email address',
    'any.required': 'Email is required'
  }),
  password: Joi.string().min(8).max(100).required().messages({
    'string.min': 'Password must be at least 8 characters long',
    'any.required': 'Password is required'
  })
});

Express route using the login schema

const express = require('express');
const app = express();
app.use(express.json());

app.post('/auth/login', async (req, res) => {
  try {
    const value = await loginSchema.validateAsync(req.body, {
      abortEarly: false
    });

    res.json({
      message: 'Login data is valid',
      data: { email: value.email }
    });
  } catch (err) {
    res.status(400).json({
      message: 'Invalid login data',
      errors: err.details.map(d => d.message)
    });
  }
});

app.listen(3000);

This is a very direct express.js request validation middleware tutorial feel, but our next step will be to extract that logic into a reusable middleware function, which is much cleaner for larger apps.

Planning and Structuring Validation in Your Project

If you add validation directly inside every route, your code will quickly become hard to maintain. A better pattern is to:

  • Centralise common validation logic in middleware
  • Keep schemas in dedicated files (rules)
  • Inject the right schema per route

Building a reusable Express validation middleware

Below is an example middleware in TypeScript, placed in /src/middleware/validate.ts. You can convert it easily to JavaScript if you prefer.

import type { Request, Response, NextFunction } from "express";
import type { Schema, ValidationErrorItem } from "joi";

export function validate(schema: Schema) {
    return (req: Request, res: Response, next: NextFunction) => {

        const input = {
            ...req.query,
            ...req.params,
            ...req.body
        };

        const { error, value } = schema.validate(input, {
            abortEarly: false,
            stripUnknown: true
        });

        if (error) {
            const errors = error.details.map((detail: ValidationErrorItem) => ({
                message: detail.message,
                path: detail.path.join("."),
            }));
            return res.status(422).json({ errors });
        }

        req.body = value;

        next();
    };
}

Validators file for auth rules

Let’s create /src/validators/auth.rules.ts. It’s not required but it’s a standard practice to keep module wise validation files which will keep all request validation under single header.

import Joi from 'joi';

export const loginSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).max(100).required()
});

Not let’s utilize this both middleware and this rules set into our API.

import { Router } from 'express';
import { validate } from '../middleware/validate';
import { loginSchema } from '../validators/auth.rules';

const router = Router();

router.post('/auth/login', validate(loginSchema), (req, res) => {
  res.json({
    message: 'Logged in successfully',
    data: { email: req.body.email }
  });
});

export default router;

Now every new route can plug in its own Joi schema. You keep your controllers simple and your validation logic fully reusable.

File structure example

The final file structure looks like this into production ready appliation.

src/
  middleware/
    validate.ts
  validators/
    auth.rules.ts
    user.rules.ts
  routes/
    auth.routes.ts
    user.routes.ts
  index.ts

It’s reusable, easy to maintain and consistency. You change validation rules in one place without touching routes or controllers.

Advanced Joi Techniques for Real-World APIs

Let’s export few advance validation methods that are more than basic string() and number() checks. Joi offers powerful tools such as custom, external, and extensions.

Custom inline validation with .custom()

Use .custom() when built-in rules are not enough. For example, you might forbid passwords that contain your brand name.

const Joi = require('joi');

const passwordSchema = Joi.string()
  .min(8)
  .custom((value, helpers) => {
    if (value.toLowerCase().includes('password')) {
      return helpers.error('string.forbiddenPassword');
    }
    return value;
  })
  .messages({
    'string.forbiddenPassword': 'Password cannot contain the word password'
  });

const schema = Joi.object({
  password: passwordSchema
});

This kind of rule helps you model real business requirements and security policies in a clear way.

Async validation with .external() for database checks

Sometimes you must validate data against a database or external API. A classic example is checking whether an email already exists before registration.

const Joi = require('joi');

const registerSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required()
}).external(async value => {
  const exists = await findUserByEmail(value.email);
  if (exists) {
    throw new Error('Email is already registered');
  }
});

async function validateRegisterInput(body) {
  return registerSchema.validateAsync(body, {
    abortEarly: false
  });
}

You call validateAsync and, if the external check fails, the promise rejects with the error you throw. This works well for uniqueness checks and other async rules.

Creating reusable Joi extensions for custom rules

For repeated and reusable custom logic many times, you can create a Joi extension. For example, you may want a reusable postcode rule.

const JoiBase = require('joi');

const Joi = JoiBase.extend(joi => ({
  type: 'string',
  base: joi.string(),
  messages: {
    'string.postcode': 'Postcode must be 5 digits'
  },
  rules: {
    postcode: {
      validate(value, helpers) {
        if (!/^\d{5}$/.test(value)) {
          return helpers.error('string.postcode');
        }
        return value;
      }
    }
  }
}));

const addressSchema = Joi.object({
  street: Joi.string().required(),
  postcode: Joi.string().postcode().required()
});

Now you can call .postcode() anywhere in your project. This keeps your schemas expressive and easy to read.

Conclusion

You have seen how Request Validation in Node.js and Express can protect your API, improve user experience, and keep your codebase tidy. Using Joi, you will describe your needs clearly, from the simple login form up to complex real-world payloads.

We started with basic schemas, then moved on to a reusable validation middleware. Lastly, we looked at some advanced features like custom rules, async checks, and extensions. What this pattern will allow you to do is validate Node.js API input using Joi in a way that scales both your team and your application.