DEV Community

Cover image for Injecting request object to a custom validation class in NestJS
Krzysztof Szala
Krzysztof Szala

Posted on • Updated on

Injecting request object to a custom validation class in NestJS

I'm a big fan of how NestJS handle validation using class-validator library. There are many advantages of using an external library for validation. For most of the typical cases default integration via ValidationPipe is good enough. But as you know, daily work likes to verify and challenge us.

A few days ago I had a specific need – I needed to validate something with ValidatorPipe and class-validator library, but one of the validation factors, was user ID. In this project, user ID is pulled out from JWT token, during the authorization process, and added to the request object.

My first thought was – just use the Injection Request Scope, like we can do it in NestJS services:

constructor(@Inject(REQUEST) private request: Request) {}
Enter fullscreen mode Exit fullscreen mode

Obviously – it doesn't work, otherwise this article wouldn't be here. Here is a short explanation made by NestJS creator, Kamil Myśliwiec:

image

Ok. So, there is basically no simple way to get request object data in custom validation constraint. But there is a way around! Not perfect, but it works. And if it can't be pretty, at least it should do its job. What steps we need to take, to achieve it?

  1. Create Interceptor, which will add the User Object to the request type you need (Query, Body or Param)
  2. Write your Validator Constraint, Extended Validation Arguments interface, use the User data you need.
  3. Create Pipe, which will strip the request type object from User data context.
  4. Create the appropriate decorators, one for each type of request.
  5. Use newly created decorators in Controllers, when you need to "inject" User data to your validation class.

Not great, not terrible. Right?
image

Interceptor

Create Interceptor, which will add User Object to request type you need (Query, Body or Param). For the demonstration purposes, I assume you store your User Object in request.user attribute.

export const REQUEST_CONTEXT = '_requestContext';

@Injectable()
export class InjectUserInterceptor implements NestInterceptor {
  constructor(private type?: Nullable<'query' | 'body' | 'param'>) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();

    if (this.type && request[this.type]) {
      request[this.type][REQUEST_CONTEXT] = {
        user: request.user,
      };
    }

    return next.handle();
  }
}
Enter fullscreen mode Exit fullscreen mode

Custom validation decorator

Write your Validator Constraint and custom decorator, Extended Validation Arguments interface, use the User data you need.

@ValidatorConstraint({ async: true })
@Injectable()
export class IsUserCommentValidatorConstraint implements ValidatorConstraintInterface {
  constructor(private commentsRepository: CommentsRepository) {}

  async validate(commentId: number, args?: ExtendedValidationArguments) {
    const userId = args?.object[REQUEST_CONTEXT].user.id;

    if (userId && Number.isInteger(commentId)) {
      const comment = await this.commentsRepository.findByUserId(userId, commentId); // Checking if comment belongs to selected user

      if (!comment) {
        return false;
      }
    }

    return true;
  }

  defaultMessage(): string {
    return 'The comment does not belong to the user';
  }
}

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

If you don't know how to inject dependencies into a custom validator in class-validator library, this article can help you.

My ExtendedValidationArguments interface looks like this:

export interface ExtendedValidationArguments extends ValidationArguments {
  object: {
    [REQUEST_CONTEXT]: {
      user: IUser; // IUser is my interface for User class
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

It allows me to use valid typing in ValidatorConstraint. Without it, TypeScript will print out an error, that the _requestContext property doesn't exist.

Stripping Pipe

Create Pipe, which will strip the request type object from User data context. If we don't do that, our DTO object will contain attached previously request data. We don't want that to happen. I'm using here one of the lodash function – omit(). It allows removing chosen properties from an object.

@Injectable()
export class StripRequestContextPipe implements PipeTransform {
  transform(value: any) {
    return omit(value, REQUEST_CONTEXT);
  }
}
Enter fullscreen mode Exit fullscreen mode

New decorators

Creating new decorators is not necessary, but it's definitely a more clean and DRY approach than manually adding Interceptors and Pipes to the methods. We're going to use NestJS built-in function – applyDecorators, which allows merging multiple different decorators into a new one.

export function InjectUserToQuery() {
  return applyDecorators(InjectUserTo('query'));
}

export function InjectUserToBody() {
  return applyDecorators(InjectUserTo('body'));
}

export function InjectUserToParam() {
  return applyDecorators(InjectUserTo('params'));
}

export function InjectUserTo(context: 'query' | 'body' | 'params') {
  return applyDecorators(UseInterceptors(new InjectUserInterceptor(context)), UsePipes(StripRequestContextPipe));
}
Enter fullscreen mode Exit fullscreen mode

To add your user data, just decorate your controller's method with one of the above decorators.

  @InjectUserToParam()
  async edit(@Param() params: EditParams){}
Enter fullscreen mode Exit fullscreen mode

Now, if you wanted to use your IsUserComment decorator in EditParams, you will be able to access injected user data.

export class EditParams {
  @IsUserComment()
  commentId: number;
}
Enter fullscreen mode Exit fullscreen mode

And that's all! You can use this method, to add any data from the request object to your custom validation class. Hope you find it helpful!

You can find an example repository on my GitHub.

In case you use in your ValidationPipe whitelist: true parameter, and above example doesn't work for you – check this issue.

This article is highly inspired by the idea I've found in this comment on GitHub.

PS. It's just proof of concept, and this comment ownership validation is a simple example of usage.

Top comments (20)

Collapse
 
walidgerges profile image
Walid-Gerges

You can go a step forward and use request context.

npmjs.com/package/@medibloc/nestjs...

Thanks for your tips, it helped me.

Collapse
 
monirul017 profile image
Monirul Islam

this is not working ValidationArguments does not hold REQUEST_CONTEXT

Collapse
 
avantar profile image
Krzysztof Szala

There was obviously typo in decorator composition class. Instead of AddUseTo, there should be InjectUserTo. Please try if it works for you now.

Collapse
 
avantar profile image
Krzysztof Szala • Edited

Are you sure you created interceptor, which injects the REQUEST_CONTEXT? I'm using similar code in my production-ready app and it works for me.

Collapse
 
monirul017 profile image
Monirul Islam

yes i am using but its not working btw.I did exactly the way you said

Thread Thread
 
monirul017 profile image
Monirul Islam

even console logged the args but REQUEST_CONTEXT returns undefined

Thread Thread
 
siavash_habil profile image
Siavash Habil

@monirul017 @avantar
I had exactly same problem and after investigating it is happening because of {whitelist: true} of ValidationPipe and when you set it to false it will working properly but I didn't continue in this way because I want to whitelist the properties so temporary I added _requestContext to the related DTO file that I used in my controller and added it as an @IsOptional() decorator

import { REQUEST_CONTEXT } from '../../../interceptors/inject-request-param.interceptor';


  @IsOptional()
  \[REQUEST_CONTEXT\]: any;
Enter fullscreen mode Exit fullscreen mode

Ignore "\" from the above code.

For others trying to use this article as a solution continue using from attached repository.

At the end thank you @avantar for your solution.

Thread Thread
 
avantar profile image
Krzysztof Szala

Thank you, @siavash_habil! This can be helpful as well.

github.com/AvantaR/nestjs-validati...

Thread Thread
 
siavash_habil profile image
Siavash Habil

@avantar This is almost happened at the same time for both of us because I solved it about 20 hours ago. :)
Thank you for sharing.

Thread Thread
 
avantar profile image
Krzysztof Szala

What a coincidence! Magic 🎉

Collapse
 
tkssharma profile image
tkssharma

this is awesome but not sure if i want to use this !! not a clean solution

Collapse
 
avantar profile image
Krzysztof Szala

Totally agree, but sometimes we need to do something dirty way 🤷‍♂️ Thanks for your feedback! 🙏

Collapse
 
dolphine_678a2ce7223 profile image
Dolphine Dev • Edited

When using @ValidateNested(), the args?.object[REQUEST_CONTEXT] is undefined because the context passed to the nested validation is not the same as the one at the top level.

Collapse
 
wahyubaskara profile image
Wahyu Baskara

ValidationArguments does not hold REQUEST_CONTEXT when using nested validation object

Collapse
 
toetet_aungmyint_05027d profile image
Xjsny

It didn’t work if I request data with multipart/form-data. Is there any solution for form data request.

Collapse
 
tidi profile image
htdai

stackoverflow.com/a/75139788

Maybe it's late but I found the solution for inject current user into body when using form-data
I have tested and it works perfectly for me

Collapse
 
ssukienn profile image
Szymon Sukiennik

Did you ever managed to port a similar approach to graphql in Nest? Namely, to have access to part or even whole gql context inside validator or validation rule?

Collapse
 
avantar profile image
Krzysztof Szala

Hi!

No, I haven't tried to use it with GraphQL, sorry.

Collapse
 
davidn96 profile image
davidN96

Hey! Thank you for a great article!

Collapse
 
avantar profile image
Krzysztof Szala

Thanks! Glad you like it! 🙏