Simplifying Data Validation in Express.js Routes with Class Validator Decorators

Validating user input is a crucial part of building robust and secure web applications. In Express.js, the process of validating incoming data can be simplified and streamlined using the class-validator library. This article will demonstrate how to use class-validator in an Express.js application to validate request bodies, URL parameters, and query parameters with ease.

Introduction to Class Validator

class-validator is a powerful library that provides decorators and validation functions to validate JavaScript objects based on defined constraints. It works seamlessly with TypeScript and provides a declarative way to define validation rules using decorators.

Setting Up the Express.js Application

Before we dive into the validation process, let's set up a basic Express.js application. We'll use the express and class-validator packages, so make sure to install them by running the following command:

npm install express class-validator

Now, let's create a new file named app.ts tsconfig.json and include the following code:

tsconfig.json

{
  "compilerOptions": {
    "target": "es2021",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "strictNullChecks": false,
    "declaration": true,
    "noImplicitAny": false, 
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

app.ts

import { IsString, IsNotEmpty, MinLength, ValidationError, validate } from "class-validator";
import Express, { NextFunction, Request, Response } from "express";

const app = Express();

// Express middleware for parsing request bodies
app.use(Express.json());
app.use(Express.urlencoded({ extended: true }));

// Your code goes here

app.listen(1000, () => {
  console.log("Server started");
});

In the above code, we import the necessary modules and set up the basic Express.js application. We'll continue writing the validation logic within the provided Your code goes here section.

Creating a Validation DTO (Data Transfer Object)

To validate the request data, we'll define a validation DTO class using the decorators provided by class-validator. This DTO class will represent the structure of the data we expect to receive in the request.

In this example, we'll create a CreateUserRequest class to validate the request body. The CreateUserRequest class should have the name and password properties, each decorated with appropriate validation decorators.

Here's an example implementation of the CreateUserRequest class:

export class CreateUserRequest {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsString()
  @MinLength(6)
  @IsNotEmpty()
  password: string;
}

In the above code, we use decorators like @IsString(), @IsNotEmpty(), and @MinLength() to define validation rules for the name and password properties.

Implementing the Validation Middleware

To integrate the validation logic into our Express.js application, we'll create a validation middleware function. This middleware function will be responsible for validating the request data based on the provided DTO class.

Here's the implementation of the validation middleware:

export function Body<T extends object>(dtoClass: new () => T) {
  return validationMiddleware(dtoClass, "body");
}
export function Param<T extends object>(dtoClass: new () => T) {
  return validationMiddleware(dtoClass, "params");
}
export function Query<T extends object>(dtoClass: new () => T) {
  return validationMiddleware(dtoClass, "query");
}

export function validationMiddleware<T extends object>(
  dtoClass: new () => T,
  body: string
) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const dtoInstance = new dtoClass();
    const requestBody = {
      body: req.body,
      query: req.query,
      params: req.params,
    };
    Object.assign(dtoInstance, requestBody[body]);
    requestBody[body] = dtoInstance;
    const errors: ValidationError[] = await validate(dtoInstance,{
      forbidUnknownValues:true,
      whitelist: true,
      forbidNonWhitelisted: true,
    });

    if (errors.length > 0) {
      const validationErrors = errors.map((error) => ({
        [error.property]: Object.values(error.constraints || {}),
      }));

      const errorMessage = "Invalid data provided";
      const errorResponse = { message: errorMessage, errors: validationErrors };

      return res.status(422).json(errorResponse);
    }

    next();
  };
}

In the above code, the validationMiddleware function takes two arguments: dtoClass (the DTO class to validate against) and body (the request property to validate, such as "body", "params", or "query"). Inside the middleware function, we create an instance of the DTO class and assign the corresponding request property values to it.

We then use the validate function from class-validator to validate the DTO instance against the defined validation rules. If any validation errors occur, we format the errors and send an appropriate error response.

Additionally, we check for unnecessary fields in the request data that are not present in the DTO class. If any such fields are found, we return an error response indicating the presence of unnecessary fields.

Using the Validation Middleware in Routes

Now that we have the validation middleware implemented, we can use it in our Express.js routes. Let's create a route to handle a POST request and validate the request body using the CreateUserRequest DTO class.

Here's an example implementation of the route:

app.post("", Body(CreateUserRequest), (req, res) => {
  console.log(req.body);
  return res.send("ok");
});

In the above code, we use the Body decorator function, which internally calls the validationMiddleware function, passing the CreateUserRequest DTO class and the string "body" to specify that we want to validate the request body.

When a POST request is made to this route, the middleware will automatically validate the request body based on the defined validation rules in the CreateUserRequest class. If the validation passes, the route handler function will be executed.

Full code


import { IsString, IsNotEmpty, MinLength, ValidationError, validate } from "class-validator";
import Express, { NextFunction, Request, Response } from "express";

const app = Express();

export class CreateUserRequest {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsString()
  @MinLength(6)
  @IsNotEmpty()
  password: string;
}

function validateDecorator<T extends object>(dtoClass: new () => T, property: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
      const dtoInstance = new dtoClass();
      const requestBody: any = {
        body: req.body,
        query: req.query,
        params: req.params,
      };
      Object.assign(dtoInstance, requestBody[property]);
      requestBody[property] = dtoInstance;
      const errors: ValidationError[] = await validate(dtoInstance, {
        forbidUnknownValues: true,
        whitelist: true,
        forbidNonWhitelisted: true,
      });

      if (errors.length > 0) {
        const validationErrors = errors.map((error) => ({
          [error.property]: Object.values(error.constraints || {}),
        }));

        const errorMessage = "Invalid data provided";
        const errorResponse = { message: errorMessage, errors: validationErrors };

        return res.status(422).json(errorResponse);
      }

      return originalMethod.call(this, req, res, next);
    };

    return descriptor;
  };
}

app.use(Express.json()) // add body parsing middleware
  .use(Express.urlencoded({ extended: true }));

class UserController {
  @validateDecorator(CreateUserRequest, "body")
  @validateDecorator(CreateUserRequest, "query")
  createUser(req: Request, res: Response) {
    console.log(req.body);
    return res.send("ok");
  }
}

const userController = new UserController();

app.post("", userController.createUser.bind(userController));

app.listen(1000, () => {
  console.log("server started");
});

Conclusion

In this article, we explored how to simplify data validation in an Express.js application using the class-validator library. We learned how to define a validation DTO class with decorators, implement a validation middleware function, and utilize the middleware in our Express.js routes.

By leveraging the power of class-validator, we can ensure that incoming data meets our application's requirements and respond with appropriate error messages when validation fails. This approach helps us build more robust and secure Express.js applications with ease.