NestJS: How to inject service to validator constraint class

Updated: December 31, 2023 By: Guest Contributor Post a comment

Introduction

Understanding how to leverage dependency injection in NestJS can vastly simplify the process of implementing custom validation that uses services within your application. This tutorial will guide you through the steps needed to inject services into validator constraint classes using the latest NestJS and TypeScript syntax.

Setting the Stage

Let’s start by setting up a new custom validator that would potentially require some data fetching or complex logic provided by a service. In NestJS, you can create custom validators by implementing the ValidatorConstraintInterface and using the @ValidatorConstraint decorator.

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

@ValidatorConstraint({ name: 'customValidator', async: false })
export class CustomValidator implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    // validation logic goes here
    return true; // or 'false' if validation failed
  }

  defaultMessage(args: ValidationArguments) {
    // a default message to be shown when validation fails
    return 'Validation failed!';
  }
}

In the code example above, we would like to inject a service into our CustomValidator. The standard dependency injection approach does not work right out of the box with custom validators. Without the correct setup, NestJS does not manage the lifecycle of validator instances, meaning that typical constructor injection will fail. However, there’s a pattern we can follow to work around this.

Injecting Services into Validator Classes

To enable service injection, we need to make our validator class a provider that can be managed by NestJS’s dependency injection system. This requires registering the custom validator as a global module so that it can be used anywhere in our application without the need to import its module everywhere.

We begin by creating a service that we want to inject:

import { Injectable } from '@nestjs/common';

@Injectable()
export class MyService {
  // Some service logic might go here
  public isValueValid(value: any): boolean {
    // Assume we have some logic to validate the value
    return true;
  }
}

Now let’s adjust our validator to allow for service injection:

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, Injectable } from 'class-validator';
import { MyService } from './my.service';

@ValidatorConstraint({ name: 'customValidator', async: true })
@Injectable()
export class CustomValidator implements ValidatorConstraintInterface {
  constructor(private readonly myService: MyService) {}

  validate(value: any, args: ValidationArguments) {
    return this.myService.isValueValid(value);
  }

  defaultMessage(args: ValidationArguments) {
    return 'Validation failed!';
  }
}

Observe the inclusion of the @Injectable() decorator. It turns the validator into a provider. Also, note that we set the async option to true if the validation logic within the service is asynchronous.

Registering the Custom Validator

The next step is to update our application module to let NestJS know about our new injectable validator:

import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { MyService } from './my.service';
import { CustomValidator } from './custom-validator';

@Module({
  providers: [
    MyService,
    CustomValidator,
    {
      provide: APP_PIPE,
      useClass: CustomValidator
    }
  ],
})
export class AppModule {}

This module-level registration couples our custom validator with the application’s global pipes. This coupling makes it possible for NestJS to inject dependencies into the custom validator since it’s now officially part of the NestJS DI system.

Advanced Usages

In scenarios where your validation logic is more complex and spans multiple services, you might not inject a single service but rather a module. Here’s how you can achieve that by using module imports:

import { Module } from '@nestjs/common';
import { MyServiceModule } from './my-service.module';
import { CustomValidator } from './custom-validator';

@Module({
  imports: [MyServiceModule],
  providers: [CustomValidator]
})
export class ValidatorModule {}

In the above example, MyServiceModule would export MyService that the CustomValidator depends on. This modular approach maintains separation of concerns and keeps your application architecture clean and organized.

Conclusion

Injecting services into validator constraints in NestJS can make your custom validators more powerful and versatile. By turning your validators into providers with the @Injectable() decorator and appropriately registering them, you can take full advantage of the NestJS dependency injection system for cleaner and more maintainable code. Following the steps outlined in this tutorial, you can easily integrate complex validation logic that interacts with various services in your application in a seamless manner.