Better Programming

Advice for programmers.

Follow publication

Validating Complex Requests With NestJS

A practical example

Dmitry Khorev
Better Programming
Published in
8 min readOct 8, 2022

Photo by Juanjo Jaramillo on Unsplash

In this article, I want to make a deep dive and explore ways to perform complex validation for incoming requests with NestJS.

NestJS provides a great way to integrate request validation into your app with some good defaults. Its advice is to use a class-validator package that is powerful and has nice documentation and examples.

Why Is Request Validation Important?

Validation is an important step in online service security and data integrity.

It ensures you only receive data in a format that your service expects. Possibility to discard any additional data that you don’t expect.

It provides a layer of protection against malicious actors, remember you should not trust any user input, ever. Employ a zero-trust policy as much as possible.

Don’t store invalid data in the database, protect services down the pipeline from invalid data input.

What Is a “Class-Validator” Package?

It is a set of decorators that you can use with your JavaScript class properties to add validation. There are basic validation rules like @IsString() — validated field should be a string, and the ability to write fully custom validation classes and decorators.

Another good thing this package allows is the usage of the Dependency Injection container from the parent app. This is very useful for validating with external sources, for example, a User database. There’s one small trick to make it work, and save you some time googling. Check out this main.ts file:

An example of a class-validator with NestJS DI container.

The most important line here is:

useContainer(app.select(AppModule), { fallbackOnErrors: true });

This is not mentioned in the official NestJS documentation at the moment of writing this.

What Is a Complex Request?

I will define it as something that will have at least 1 array or 1 object, that will in turn include nested objects and/or arrays. Another common use case is checking validity against an external resource (DB, cache, S3, etc.).

For my example I will use an imaginary SaaS multi-tenant e-commerce API, that will allow me to:

  • register a user globally
  • place an order for a specific shop

Validating a User Create Request

To register the user, we’ll require the fields listed in the class below:

We will register users globally, without a link to any specific shop.

Our constraints are as follows:

  • name — required, is a string, should be at least three characters long.
  • email— required, should be a valid email string, should not be already registered in our database.
  • password — required, is a string, should be at least 8 characters long.

This is what my UserCreateDto will look like after adding class-validator decorators.

Let’s first review standard validation rules from the class-validator package used here:

  • @IsString() —value should be a string
  • @MinLength(N) —value should have a min length of N
  • @IsEmail() — value should be a valid email (syntax check only)

Now the interesting part is the @EmailNotRegistered() validation. This one I created to deny registering users if their email already exists in our app.

Custom validation that checks user existence by email against a 3rd party source.

This is my custom validation decorator, the process of creating one is described in the docs. However most of this code is boilerplate, let’s check the most interesting lines.

Line 11 — injects a custom provider private readonly userRepository: UserRepository.

Lines 14–15 — provide a validation function by searching the email in the UserRepository.

My UserRepository is just an in-memory store, that emulates some form of async network I/O. This is to replicate the real DB request/response lifecycle.

An in-memory store that replicates async network I/O.

The user register API endpoint looks like this:

This is all you have to do to make NestJS validate incoming requests:

  • type-hint a DTO class in your route’s signature
  • add validation decorators to your DTO class
  • turn on validation usage within main.ts (example shown above)
User register endpoint validation in action.
User register endpoint validation in action.

Nicely done. Let’s move on to the order request and validation!

Validating a Create Order Request

Okay, we’ve started with something rather simple, now let’s try to validate an order request.

Our requirements for the OrderCreateDto are:

  • should have common order properties, like a shop ID (we’re SaaS multi-tenant app), and the date when created.
  • should have customer information (customer email).
  • should have an order products list (an array), with a common structure — product ID, and quantity.
  • should have an order shipment description, which is a variable object: either Delivery or Pickup type, with various required fields.
  • finally, it should have a contact person list, with some details for each contact person.

The first, non-validated DTO that meets such requirements may look like this:

Expected create-order request object.

Let’s go over each block of this request, describe it, and add validations.

OrderCreateDto

OrderCreateDto fields.

Here are the constraints:

  • shop_id — should be in UUID format and exist in ShopRepository.
  • created_at — should be a date object or date castable.

Solution

An example of validating order-specific fields.
Validates shop ID against an external resource.

@IsUUID() — is a decorator for checking a string to be a valid UUID.

@Validate(ShopIdExistsRule) — validates the field with a custom rule (similar to writing your decorators, but a bit simpler).

@Type(() => Date) & @IsDate() — will try and cast provided input to Date and validate it’s a Date object.

Order create validation in action.
Order create validation in action.

OrderCustomerDto

OrderCustomerDto fields.

Here are the constraints:

  • email — should exist in the user repository, and should be a valid email string.
  • validate a nested object of OrderCustomerDto inside parent OrderCreateDto.

Solution

OrderCreateDto and OrderCustomerDto objects with validation decorators.

New things here:

@CustomerExists() — this is the opposite of the @EmailNotRegistered() rule I have used before. Logic is similar and we take advance of injected UserRepository.

@Type(() => OrderCustomerDto) — this is a utility line, that transforms the nested object into a class, so it can be validated by following @ValidateNested(). If you don’t transform — validation will not run.

Customer data validation in action.
Customer data validation in action.

If you get an error when starting NestJS:

ReferenceError: Cannot access ‘OrderCustomerDto’ before initialization

You need to move the OrderCustomerDto declaration before OrderCreateDto, as shown in my code example above.

OrderProductDto

OrderProductDto fields.

Here are the constraints:

  • products — should be an array, should not be empty.
  • id — should exist in the ProductRepository, and should be an integer value.
  • quantity — should be enough of this product available and should be an integer.

Solution

OrderProductDto and OrderCustomerDto objects with validation decorators.

Here's some new things:

@IsInt() — check the value to be an integer, 100.5 will fail validation.

@ArrayNotEmpty() — validates that the products array is not empty.

@ValidateNested({ each: true }) — triggers an array nested validation, so we can effectively validate from 1 to N products.

ProductIdExists and ProductIsAvailable — are custom rules that check if the product exists and if there’s enough quantity to place an order.

Product data validation in action.

OrderShipmentDto

OrderShipmentDto fields.

Here are the constraints:

  • type — should be a part of the enum (Delivery | Pickup), should be defined.
  • if the Delivery type is selected — city and address fields are required strings, not empty.
  • if the Pickup type is selected — the point_id field is required, should be an integer, not empty.

Solution

Shipment and OrderCustomerDto objects with validation decorators.

Here’s some new things:

@Equals(DeliveryTypes.DELIVERY) and @Equals(DeliveryTypes.PICKUP) — will do a strict (===) check of provided value.

Then, based on the delivery type we validate the nested object. For that, we need a bit more advanced @Type() use case.

A helper for class-transformer to type depending on the field in the request.

Since the shipment object is of variable type, we include variable type cast for validation and shipment now looks as this: shipment: DeliveryShipmentDto | PickupShipmentDto;

This allows our validation system to drop fields that are not required by specific delivery objects, i.e., you request Pickup shipment and provide city — this will now be dropped by the class-transformer package.

Shipment data validation in action.

OrderContactDto

OrderContactDto fields.

Here are the constraints:

  • name — should be a string, should be defined.
  • phone — should be a valid mobile number.
  • email — optional, if defined should be a valid email syntax.
OrderContactDto and OrderCustomerDto objects with validation decorators.

New validations are here:

@IsMobilePhone(‘en-US’) — will check the phone string to have valid syntax, amount of numbers, etc.

@IsOptional() — will only validate the email if it was provided.

Contact validation in action.

Final Solution

OK, it’s been a long path. Now, let’s see a full request object with all the validation decorators:

The final version of validation for the NestJS DTO class.

Bonus: More Common Use Cases for Validation

It’s hard to come up with an example that covers all features of request validation. So, here I will list some interesting use cases from my practice:

Payment confirmation

Use case: webhook call from a third-party card processor (PayPal, Stripe).

You usually have some form of secret signing key stored in the config and need to validate the request’s signature.

Here’s an example of a validator that uses injected NestJS config service.

Request

We will combine order id + amount + secret and compare it with the received signature.

This validation rule uses NestJS ConfigService to get the secret value.
Signature validation in action.

Conditional property validation

Use case: only validate property if another property is set to a specific value.

Task: only validate the email if subscribe is true.

An example of a validation request with an email field depending on the subscribe field’s value.
Subscribe validation in action.

Duplicate constraint validation without Database query

Use case: you have a DB constraint UNIQUE(field1 + field2), but you want to validate this before reaching DB level (and getting the store exception). Your request accepts multiple entities to store at once and it can contain duplicates.

Here’s an example DTO:

And with validation:

An example of a uniqueness validator for an array of records.
No duplicate user validation in action.

Date range constraints relative to the request

Use case: a filter with start and end dates. You expect the end date to be later than the start date.

With validation constraints:

Date validation with start/end constraint applied.
Date range validation in action.

Conclusion

NestJS and class-validator together play well for request validation. It can cover everything from the simplest to complex validation scenarios.

Another good point is that you get a standard error response structure in case of failed validation that you don’t have to code yourself — all handled by NestJS.

The repository with the test code is here: https://github.com/dkhorev/validating-complex-requests-with-nestjs

I hope this was helpful. Good luck, and happy engineering!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Dmitry Khorev
Dmitry Khorev

Written by Dmitry Khorev

Sharing my experience in software engineering (NestJS, Laravel, System Design, DevOps)

Responses (10)

Write a response