Reputation: 757
I want to apply server-side validation on my CRUD API. The entity in question is called Employee
. I am using an employee.dto
(shown below) for the create and update endpoints.
The class-validator package works fine on the create
method but ignores all rules in the DTO when I use it with Partial<EmployeeDTO>
in the update method.
Please use the code below for reference.
"class-transformer": "^0.2.3",
"class-validator": "^0.10.0",
import { IsString, IsNotEmpty, IsEmail, IsEnum } from 'class-validator';
import { EmployeeRoles } from '../../entities/employee.entity';
export class EmployeeDTO {
@IsString()
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsNotEmpty()
password: string;
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@IsNotEmpty()
fullName: string;
@IsString()
@IsNotEmpty()
@IsEnum(EmployeeRoles)
role: string;
}
import {
Controller,
Param,
Post,
Body,
Put,
UsePipes,
} from '@nestjs/common';
import { EmployeeDTO } from './dto/employee.dto';
import { EmployeeService } from './employee.service';
import { ValidationPipe } from '../shared/pipes/validation.pipe';
@Controller('employee')
export class EmployeeController {
constructor(private employeeService: EmployeeService) {}
@Post()
@UsePipes(ValidationPipe)
addNewEmployee(@Body() data: EmployeeDTO) {
return this.employeeService.create(data);
}
@Put(':id')
@UsePipes(ValidationPipe)
updateEmployee(@Param('id') id: number, @Body() data: Partial<EmployeeDTO>) {
return this.employeeService.update(id, data);
}
}
I work around I can think of is creating separate DTOs for create
and update
methods, but I don't like the idea of repeating the code.
Upvotes: 15
Views: 27715
Reputation: 1482
I ran into this exact issue and would like to share some discoveries as well as a solution.
Let's pretend we have this class:
User {
@IsNotEmpty()
name: string;
@IsOptional()
nickname?: string;
}
On POST you want all validations to apply. On PATCH (or PUT) you want only validations to apply for properties that are specified. Well, POST is no problem, in the controller do:
@Body() params: User
But, PATCH (or PUT) is problematic because there is no way to handle partial validations out of the box without applying it universally.
Here are some solutions I explored:
import { OmitType, PartialType } from '@nestjs/swagger';
UpdateUser extends PartialType(User) {}
Then, in the controller:
@Body() params: UpdateUser,
Using NestJS' PartialType does not override the class validator annotations on the parent class. So, if you have a @IsNotEmpty()
on the parent class it will still make it required.
In the controller:
@Body() params: Partial<User>
None of the class validator annotations apply. This is because generics are not supported, as explained elsewhere.
In the controller:
@Body() params: any
Then, in the controller method call class validator's validate()
directly with something like:
const transformedValue = plainToClassFromExist(new User(), params);
const errors = validate(transformedValue, {
skipUndefinedProperties: true
});
The magic here is skipUndefinedProperties
. If the property does not exist (ie is undefined) then class validator will not validate it. Note that @IsDefined() bypasses this.
This would work, but will result in lots of duplicate code.
Create a decorator called PartialBody
that reads the type information from an enhancer and then runs validations, see https://gist.github.com/josephdpurcell/d4eff886786d58f58b86107c0947e19e as an example.
Make sure validateCustomDecorators=false in your global validation pipe.
Now, there are a few variations of how to use it:
In the controller:
@PartialBody(User) params: Partial<User>
When PartialBody
runs it will retrieve the type from the passed argument. This will only validate properties that exist, and will ensure params
is a partial of User. Huzzah!
In the controller:
@PartialBody() params: Partial<User>
This fails because generics aren't supported. When the PartialBody
decorator logic is run it has access to no type information.
In the controller:
@PartialBody() params: User
While this would perform partial validations just like Variation 1, it fails because params
would be seen as a full type instead of a partial type. While writing code your editor would think that all properties are there when they may not be.
Create a new type with PartialType:
import { PartialType } from '@nestjs/swagger';
export class PartialUser extends PartialType(User) {}
In the controller:
@PartialBody() params: PartialUser
This works just like Variation 1, but you have to create an additional class. This may be desired if you wish to override class validator checks by overwriting the properties and redeclaring decorators.
Upvotes: 6
Reputation: 3488
This article opened my mind and gave me a better understanding: https://medium.com/fusionworks/api-payloads-validation-and-transformation-in-nestjs-5022ce4df225
It all started because I am lazy and don't want to write multiple classes (a create DTO and an update DTO). Plus, you make one change in one class, you have to remember make the change in the other one.
As a result, my updateDTO extends my createDTO & my initial approach was with PartialType
imported from @nestjs/mapped-types
.
Given the following example:
export class CreateEmployeeDTO {
// might wanna do this if you want to take an updateDto and transform to an createDto
@Exclude()
id?: number;
@IsString()
@IsNotEmpty()
username: string;
@IsSring()
@IsNotEmpty()
password: string;
@IsString()
@IsNotEmpty()
role: string;
}
For the UpdateEmployeeDTO I extend it using OmitType
instead of PartialType
, because I am assuming when I am updating the Employee:
Therefore, my UpdateEmployeeDTO looks as such:
export class UpdateEmployeeDTO extends OmitType(CreateEmployeeDTO, ['id', 'password']) {
@IsNumber()
@IsNotEmpty()
id: number;
@IsString()
@IsOptional()
password: string;
}
The second array passed to the OmitType
are the properties omitted from CreateEmployeeDTO.
Therefore, on update if I update the employee and not give it an username or role, my ValidationPipe will produce the appropriate error according to class-validator.
As a result, with the respect to the question, all rules in the (create) DTO will not be ignored.
Read the article above, very well written.
Upvotes: 1
Reputation: 376
In order to achieve partial validation, you can use PartialType
utility function. You can read about it here:
https://docs.nestjs.com/openapi/mapped-types#partial
You would need to create another class:
export class UpdateEmployeeDTO extends PartialType(EmployeeDTO) {}
and then in your controller, you need to replace the type of @Body data Partial<EmployeeDTO>
to UpdateEmployeeDto
. It should look like this:
@Patch(':id')
@UsePipes(ValidationPipe)
updateEmployee(@Param('id') id: number, @Body() data: UpdateEmployeeDTO) {
return this.employeeService.update(id, data);
}
Please keep in mind that you should import PartialType
from @nestjs/mapped-types
not from @nestjs/swagger
like suggested in the documentation. More about this can be found here
Upvotes: 34
Reputation: 211
For this answer, I'll take a guess and assume that you use the ValidationPipe
provided in the NestJS' documentation, or a close derivative.
Your updateEmployee
method's argument data
type is Partial
, which doesn't emit any type metadata. for the ValidationPipe
to instantiate it using the class-transformer
module, resulting in the class-validator
module to validate a plain object, and not an EmployeeDTO
.
For the validation to work, the type of the data
argument should be a class.
You could either make separate DTOs to create and update your entity, or use validation groups if you want to keep a single class.
Upvotes: 12