Validating Complex Requests With NestJS
A practical example

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:
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:
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.
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.
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)

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
orPickup
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:
Let’s go over each block of this request, describe it, and add validations.
OrderCreateDto
Here are the constraints:
shop_id
— should be in UUID format and exist inShopRepository
.created_at
— should be a date object or date castable.
Solution
@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.

OrderCustomerDto
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 parentOrderCreateDto
.
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.

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
Here are the constraints:
products
— should be an array, should not be empty.id
— should exist in theProductRepository
, and should be an integer value.quantity
— should be enough of this product available and should be an integer.
Solution
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.

OrderShipmentDto
Here are the constraints:
type
— should be a part of the enum (Delivery
|Pickup
), should be defined.- if the
Delivery
type is selected —city
andaddress
fields are required strings, not empty. - if the
Pickup
type is selected — thepoint_id
field is required, should be an integer, not empty.
Solution
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.
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.

OrderContactDto
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.
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.

Final Solution
OK, it’s been a long path. Now, let’s see a full request object with all the validation decorators:
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.
ConfigService
to get the secret value.
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
.

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:

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:

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!