DEV Community

Костя Третяк
Костя Третяк

Posted on

NestJS vs. Ditsmod: pipe features

In reality, pipes only exist in NestJS, while in Ditsmod, there is no need for a separate entity like "pipes" because the so-called FactoryProviders can easily cover similar functionality. Moreover, FactoryProviders have much broader capabilities, so pipes are merely a subset of what they can achieve.

Pipes in NestJS

As stated in the NestJS documentation, pipes perform two functions: transformation and validation.

In the following example, a controller method sets up a route with the parameter id, which is of type number:

import { Сontroller, Get, Param, ParseIntPipe } from '@nestjs/common';

@Сontroller()
class ExampleController {
  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    return this.catsService.findOne(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the first argument passed to the @Param() decorator is the name of the parameter to check, in this case, id. The second argument is the ParseIntPipe class. The pipe class itself may contain code like the following:

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    const num = Number(value[metadata.data]);
    if (isNaN(num)) {
      const msg1 = `"${data}" in the path parameters must have the Number data type.`;
      throw new TypeError(msg1);
    }
    return num;
  }
}
Enter fullscreen mode Exit fullscreen mode

In the pipe class, the transform method must be implemented, which accepts the raw value from the client as the first argument and metadata as the second argument, following this interface:

interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>; // String, Number, etc.
  data?: string; // property name, for example 'id'
}
Enter fullscreen mode Exit fullscreen mode

What the transform method returns will be received by the controller method.

In addition to the usage described above, NestJS has another use of pipe application:

@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
Enter fullscreen mode Exit fullscreen mode

In this example, a controller method sets up a route that accepts a request body. Here, the pipe instance is passed directly in the @UsePipes() decorator. This way, the pipe's transform method receives the request body and validates it based on a prepared Zod schema.

As of now, NestJS still does not support pipes for request headers.

"Pipes" in Ditsmod

Unlike NestJS, Ditsmod does not have a specific architectural entity like pipes. Instead, it has FactoryProviders, which are passed into the module or controller metadata like this:

import { rootModule } from '@ditsmod/core';
import { ParseIntPath } from './path-params-parsers.js';

@rootModule({
  // ...
  providersPerReq: [{ token: ParseIntPath, useFactory: [ParseIntPath, ParseIntPath.prototype.transform] }],
})
class AppModule {}
Enter fullscreen mode Exit fullscreen mode

As you can see, we pass the ParseIntPath class as the provider's token, and its value will be what its transform() method returns. The ParseIntPath code could look like this:

import { inject, AnyObj, PATH_PARAMS, CTX_DATA, CustomError, Status } from '@ditsmod/core';

export class ParseIntPath {
  transform(@inject(PATH_PARAMS) pathParams: AnyObj, @inject(CTX_DATA) propertyName: string) {
    const num = Number(pathParams[propertyName]);
    if (isNaN(num)) {
      const msg1 = `"${propertyName}" in the path parameters must have the Number data type.`;
      throw new CustomError({ msg1, status: Status.BAD_REQUEST, level: 'debug' });
    }
    return num;
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, the @inject decorator is used before the method parameters, asking Ditsmod to provide values for the PATH_PARAMS and CTX_DATA tokens. The first token is self-explanatory, while the second is a special token used to get contextual data passed to the @inject decorator alongside ParseIntPath.

In the following example, a controller method sets up a route with the parameter id, which is of type number:

import { controller, route, Res } from '@ditsmod/core';
import { ParseIntPath } from './path-params-parsers.js';

@controller()
class ExampleController {
  @route('GET', ':id')
  findOne(res: Res, @inject(ParseIntPath, 'id') id: number) {
    res.sendJson({ id });
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that the @inject decorator takes a string as the second argument, representing the path parameter name. This way, ParseIntPath retrieves the value via the CTX_DATA token (this feature is available in Ditsmod starting from v2.59.1). Keep in mind that when @inject receives two arguments, the DI will not use a cache to search for the token's value.

If we compare this to the NestJS controller mentioned above, the syntax of the two controllers seems similar. However, using ParseIntPath as a provider to function as a "pipe" is just one way to utilize providers in Ditsmod. Methods of controllers in Ditsmod bound to routes can request any other provider.

Currently, this is not possible in NestJS, as controller methods can only request data via decorators like @Param, @Query, and @Body. Even the @Headers decorator is still unavailable in NestJS, and the framework's author has not yet expressed an intention to add it.

Conclusion

Compared to NestJS, Ditsmod offers broader provider capabilities, eliminating the need for a separate entity like pipes, which require additional learning and cannot be exported from a module. Moreover, controller methods in Ditsmod are not subject to the same restrictions found in NestJS. Essentially, Ditsmod's controller methods can receive any providers, just like in constructors of controllers or services. This adds consistency and simplicity to Ditsmod's architecture.

Top comments (0)