NestJS performs input validation using decorators, pipes, and the class-validator utility. For example, if validation is required for path-parameters or query-parameters, the @Param()
or @Query()
decorators are used for this, respectively, along with the necessary pipes. And if we validate the request body, we need to use the @Body()
decorator, as well as the decorators provided by the class-validator utility. Surprisingly, NestJS does not provide similar support for validating headers or cookies. Unlike NestJS, Ditsmod has such support, it uses OpenAPI to describe validation models, and extensions, interceptors, and ajv utility for validation itself.
This post compares NestJS v9.2.0 and Ditsmod v2.54.0. A finished example of validation with the Ditsmod application can be viewed at github. I am the author of Ditsmod.
Turn on validation in NestJS
To globally turn on a native validation pipe in NestJS, you need to use app.useGlobalPipes()
method in the main.ts file:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
// ...
}
bootstrap();
Note that the pipe is passed outside the module system, so the ValidationPipe
instance cannot be obtained as a dependency via DI. The NestJS documentation suggests a workaround for this:
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
But right away it is emphasized that such a pipe transfer is global and cannot be done at the module level. In my opinion, this looks rather strange, and also severely limits the modularity of NestJS applications.
Turn on validation in Ditsmod
Since the design of the architecture Ditsmod gives special attention to the concept of modularity, this framework allows you to import a ValidationModule
into any single module:
import { featureModule } from '@ditsmod/core';
import { ValidationModule } from '@ditsmod/openapi-validation';
@featureModule({
imports: [
ValidationModule
// ...
],
})
export class SomeModule {}
ValidationModule
can also be imported globally in the root module:
import { featureModule } from '@ditsmod/core';
import { ValidationModule } from '@ditsmod/openapi-validation';
@featureModule({
imports: [
ValidationModule
// ...
],
exports: [
ValidationModule
// ...
],
})
export class AppModule {}
Hint: "When Ditsmod exports any module from the root module, it essentially makes it global."
Note that there are currently no pipes in Ditsmod, and there is no need to introduce them into the framework architecture yet, as HTTP interceptors paired with extensions serve their role well.
Description of path-parameters and query-parameters for validation in NestJS
The following example is taken from NestJS documentation, it shows how to describe the path parameter id
and the query parameter sort
directly in the controller method:
@Get(':id')
findOne(
@Param('id', ParseIntPipe) id: number,
@Query('sort', ParseBoolPipe) sort: boolean,
) {
console.log(typeof id === 'number'); // true
console.log(typeof sort === 'boolean'); // true
return 'This action returns a user';
}
As you can see, pipes in NestJS, in addition to validation, also perform transformation to the specified types.
Note that headers and cookies cannot be handled similarly in NestJS. While there are workarounds for this, it's still pretty weird and doesn't add consistency to NestJS.
Description of path-parameters and query-parameters for validation in Ditsmod
Since automatic validation in Ditsmod is based on the OpenAPI documentation, you must first create a model for it:
import { property } from '@ditsmod/openapi';
class Params {
@property()
id: number;
@property()
sort: boolean;
@property()
otherQueryParam: string;
@property()
header1: string;
@property()
cookie1: string;
}
Now we will use this model in the controller method:
import { controller, inject, PATH_PARAMS, QUERY_PARAMS, Res } from '@ditsmod/core';
import { oasRoute, parameters } from '@ditsmod/openapi';
import { Params } from './models';
interface PathParams {
id: number;
}
interface QueryParams {
sort: boolean;
}
@controller()
export class FirstController {
@oasRoute('GET', ':id', {
parameters: new Parameters()
.required('path', Params, 'id')
.optional('query', Params, 'sort', 'otherQueryParam')
.optional('header', Params, 'header1')
.optional('cookie', Params, 'cookie1')
.getParams(),
})
findOne(
@inject(PATH_PARAMS) pathParams: PathParams,
@inject(QUERY_PARAMS) queryParams: QueryParams,
res: Res
) {
console.log(typeof pathParams.id === 'number'); // true
console.log(typeof queryParams.sort === 'boolean'); // true
res.send('This action returns a user');
}
}
As you can see, parameters in headers and cookies are described exactly the same as other parameters in OpenAPI. In this form of writing, the parameter names are controlled by TypeScript, so if you make a typo, TypeScript will tell you about it.
The transformation to the specified types also occurs if the corresponding option is passed during the import of the validation module.
Description of the request body for validation in NestJS
The description of the request body model in NestJS is similar to the description of Ditsmod models, but NestJS uses decorators provided by the utility class-validator. In NestJS, it is customary to name the request body model with the ending *Dto
(this is an abbreviation of Data transfer object):
import { IsEmail, IsNotEmpty } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsNotEmpty()
password: string;
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return 'This action adds a new user';
}
The class-validator
utility directly performs the validation itself.
Description of the request body for validation in Ditsmod
The description of the request body model in Ditsmod is done similarly to the description of parameters, but it is passed in requestBody.content
:
import { property } from '@ditsmod/openapi';
export class CreateUserDto {
@property()
email: string;
@property()
password: string;
}
This model can now be used in a controller method:
import { controller, inject, Req, Res } from '@ditsmod/core';
import { getContent, oasRoute, property } from '@ditsmod/openapi';
import { HTTP_BODY } from '@ditsmod/body-parser';
import { CreateUserDto } from './models';
@controller()
export class FirstController {
@oasRoute('POST', 'users', {
requestBody: {
content: getContent({ mediaType: 'application/json', model: CreateUserDto }),
},
})
create(res: Res, @inject(HTTP_BODY) body: CreateUserDto) {
res.sendJson(body); // Returns body back to the client
}
}
In the @ditsmod/openapi-validation
module, at the moment, the utility ajv directly performs the validation itself.
Conclusion
As the author of Ditsmod, I tried to write this framework in a modular and consistent way. It seems to me that NestJS clearly loses in this regard to Ditsmod in many points, in particular to validation. If you think there are significant advantages of NestJS over Ditsmod, please write about it in the comments.
Top comments (0)