DEV Community

Cover image for Custom validation with database in NestJS
Krzysztof Szala
Krzysztof Szala

Posted on • Updated on

Custom validation with database in NestJS

NestJS is an outstanding web framework which supports natively TypeScript out of the box. Its maintainers and community provides great and large documentation, which leads us by hand through the most important parts of the framework.

But when you start to write an application for serious, you figure out quickly, it doesn't coverage some very typical cases (at least for me).

Framework provides several ways for validation data coming with requests. We can basically use Pipes, a feature called schema-based validation (using joi library) or integration with class-validator library through ValidatorPipe. And the last one is my favorite one. Why? The main reason is that you can keep your whole validation definitions outside the controller. It's a great way to separate different concerns.

Class-Validator library is a powerful tool, which brings whole set of differential validation decorators like @Length(10, 20), @IsInt(), @Contains('example') etc. I won't introduce to you how to use basic validation in NestJS, documentation explains it quite accurately.

But what if you would like to create your own validator, and use it with class-validator library? Easy, just quick look at the documentation and you can write your own rules and can use it with @Validate() decorator. Better! It's fabulously simple to write own decorators and have your whole request validation class consists.

Problems start when we are forced to check something for example in persistent storage, like database. In short ā€“ we must inject some dependency responsible for interacting with the database. Dependency like e.g. UserRepository which is obviously responsible for users entities.

Luckly for us, class-validator provides very handy useContainer function, which allow to set the container to be used by class-validor library.

So add this code in your main.ts file (app variable is your Nest application instance):

useContainer(app.select(AppModule), { fallbackOnErrors: true });
Enter fullscreen mode Exit fullscreen mode

It allows class-validator to use NestJS dependency injection container.

Then we can create a repository, which will query our database:

@Injectable()
class UserRepository {
  async getOneOrFail(userId: number): Promise<UserEntity> {
    // some code which fetch user entity or throw exception
  }
}
Enter fullscreen mode Exit fullscreen mode

Ok, let's write a Validator Constraint which will hold our own validation logic. As you can see, our dependency is simply injected into the class constructor:

@ValidatorConstraint({ name: 'UserExists', async: true })
@Injectable()
export class UserExistsRule implements ValidatorConstraintInterface {
  constructor(private usersRepository: UsersRepository) {}

  async validate(value: number) {
    try {
      await this.usersRepository.getOneOrFail(value);
    } catch (e) {
      return false;
    }

    return true;
  }

  defaultMessage(args: ValidationArguments) {
    return `User doesn't exist`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Don't forget to declare you injectable classes as providers in the appropriate module.

Now you can use your custom validation constraint. Simply decorate class property with @Validate(UserExistsRule) decorator:

export class User {
  @IsInt()
  @Validate(UserExistsRule);
  readonly id: number;
}
Enter fullscreen mode Exit fullscreen mode

If the user doesn't exist in the database, you should get error with default message "User doesn't exist". Although using @Validate() is fine enough, you can write your own decorator, which will be much more convenient. Having written Validator Constraint is quick and easy. We need to just write decorator factory with registerDecorator() function.

export function UserExists(validationOptions?: ValidationOptions) {
  return function (object: any, propertyName: string) {
    registerDecorator({
      name: 'UserExists',
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      validator: UserExistsRule,
    });
  };
}
Enter fullscreen mode Exit fullscreen mode

As you can see, you can either write new validator logic, or use written before validator constraint (in our case ā€“Ā UserExistsRule class).

Now we can go back to our User class, and use @UserExists validator instead of @Validate(UserExistsRule) decorator.

export class User {
  @IsInt()
  @UserExists();
  readonly id: number;
}
Enter fullscreen mode Exit fullscreen mode

Hope this little article will help you with many common scenarios during your application development with NestJS framework. I use that technique almost every day!

Top comments (20)

Collapse
 
smolinari profile image
Scott Molinari

Hey. Nice article. Two things.

If the user exists in the database, you should get error with default message "User doesn't exist".

Did you possibly mean to write the error as "User already exists"?

Also, I feel validation is very often based on business rules. And as such, it would be a lot of running to the programmer to ask him or her to update decorators. How would one take rules from a database (created by business) and use them dynamically within the code? I think that solution would be a whole lot smarter. The same goes for authorization rules, don't you think?

Scott

Collapse
 
avantar profile image
Krzysztof Szala • Edited

Hi Scott, thank you for your comment!

Did you possibly mean to write the error as "User already exists"?

No, it tests if user exists in database, if it doesn't validator should return error, that "user doesn't exist". It can be useful, when you check for example user id for crud operations, and don't want to run any logic, if input data isn't valid.

Also, I feel validation is very often based on business rules. And as such, it would be a lot of running to the programmer to ask him or her to update decorators. How would one take rules from a database (created by business) and use them dynamically within the code? I think that solution would be a whole lot smarter. The same goes for authorization rules, don't you think?

To be honest, I'm not the biggest fan of storing business logic in the database. There are several reasons for that. I have some experience with it, and it never ends well. Business logic stored outside code base is much harder to test, and it isn't possible to test it with unit-test (as unit tests should be database independent). For me business logic changes only when code is changed. This is why we write unit tests, right? They test business logic. If there is no business logic in our services (like simple CRUD operations), unit tests look like testing mocked data.

Ok, you can store some data in DB, like for example ā€“ a user role, and list of access routes. But the business logic is still in the code base. Roles, and theirs access routes, are only input data, for authorization rules.

Not sure if that what I'm saying is clear and makes sense :D

Regards

Collapse
 
smolinari profile image
Scott Molinari • Edited

Ok. Looked again at the code. My bad on the message. I was thinking in terms of creation of the user (and the user being unique), whereas the code is talking about the retrieval of the user. :) What I was thinking as a validation would be on a create or update DTO.

I see what you are saying with testing, however, that's also the point I'm trying to make to some degree I believe. Unit tests should test that the validation system works, but the conditions of the validation are meta to the "unit". The conditions are and must be variable as business changes all the time and thus, those rules themselve don't need testing. The system/ unit should take a set of rules and run validation on that set of rules. What's in the rules doesn't matter, the proper validation does.

The "testing" of the rules would lie solely with the business, since they are setting them. And because the rules are metadata to the system, it has to be purely manual on their part. Or, it could be within integration testing, but that would also be insanely complex, depending on the validations needed. :D

The goal I'm shooting for is to have a system of validation (and authorization) that is mainly independent of the programmer. It would be designed for a larger system with complex business operations. I've seen it done from a user's perspective. I'm just not quite sure how to make it happen with Nest. But, your article gave me more insight into the topic. Thanks!

Thread Thread
 
avantar profile image
Krzysztof Szala • Edited

It's interesting point of view Scott, but I'll stand by my opinion, that validation rules belong to the code base, not to the storage ;) Anyway, thanks for great comments, appriciate that! Have a nice weekend!

Collapse
 
svarupdesai profile image
Svarup Desai

one thing that not mentioned is that, we have to add in app.module.ts

@Module({
  imports: ...,
  controllers: [AppController],
  providers: [
    AppService,
    ...,
    // Line below added
    UserExistsRule,
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
simplenotezy profile image
Mattias Fjellvang

This is a great article and just what I was looking for. However, is there a way to provide the container (using useContainer(app.select(AppModule), { fallbackOnErrors: true });) in the app.module.ts instead of in the main.ts file? Because I'd like this to be testable, and if I define it inside app.module.ts, my e2e tests would not function with my custom decorators.

Collapse
 
davidn96 profile image
davidN96

Hey! Great article! Could you share a link to repo with code from this article? I'm newbie in Nest, I have a problem with database dependency injection. I'll be very thankful

Collapse
 
avantar profile image
Krzysztof Szala

Thank you. I'll try to upload some repos, when I find some time. What kind of problems do you have with database DI? Maybe I can help here. Regards.

Collapse
 
davidn96 profile image
davidN96

Hey! Thank you for your response! Well, I've found the problem. I've accidentally imported useContainer from typeorm instead of class-validator (VSC hints). Now everything works great. Thank you for a great article!

Thread Thread
 
avantar profile image
Krzysztof Szala

That's great, happy to help! :)

Collapse
 
netojose profile image
JosƩ Neto

Awesome, thanks!

Collapse
 
avantar profile image
Krzysztof Szala

Youā€™re welcome!

Collapse
 
huynhnguyentoan_8 profile image
huynh nguyen toan

Thanks bro.

Collapse
 
lordememphis profile image
Tafadzwa Muteke

Thank you, Krzysztof!

Collapse
 
avantar profile image
Krzysztof Szala

Glad I could help!

Collapse
 
oesron profile image
Esron Silva

Awesome content right here. You saved me a ton of work and time.

Collapse
 
avantar profile image
Krzysztof Szala

Iā€™m glad I could help! šŸ˜€

Collapse
 
navinmishra1717 profile image
Navin Mishra

great article. very helpful

Collapse
 
jedliu profile image
Jedediah

Signed up Dev.to just to say thank you! A great series for Nestjs! Great job!

Collapse
 
avantar profile image
Krzysztof Szala

Thank you šŸ™