NestJS vs. Ditsmod: auto-validation

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.27.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, the concept of modularity is given special attention, this framework allows you to import a ValidationModule to any individual module:

import { Module } from '@ditsmod/core';
import { ValidationModule } from '@ditsmod/openapi-validation';

@Module({
  imports: [
    ValidationModule
    // ...
  ],
})
export class SomeModule {}

ValidationModule can also be imported globally in the root module:

import { Module } from '@ditsmod/core';
import { ValidationModule } from '@ditsmod/openapi-validation';

@Module({
  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, Req, Res } from '@ditsmod/core';
import { OasRoute, Parameters } from '@ditsmod/openapi';

import { Params } from './models';

@Controller()
export class FirstController {
  constructor(private req: Req, private res: Res) {}

  @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() {
    const id: number = this.req.pathParams.id;
    const sort: boolean = this.req.queryParams.sort;

    console.log(typeof id === 'number'); // true
    console.log(typeof sort === 'boolean'); // true
    this.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;
}
This model can now be used in a controller method:

@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, Req, Res } from '@ditsmod/core';
import { getContent, OasRoute } from '@ditsmod/openapi';

import { CreateUserDto } from './models';

@Controller()
export class FirstController {
  constructor(private req: Req, private res: Res) {}

  @OasRoute('POST', 'users', {
    requestBody: {
      content: getContent({ mediaType: 'application/json', model: CreateUserDto }),
    },
  })
  create() {
    this.res.sendJson(this.req.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.