Костя Третяк
Posted on October 3, 2024
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);
}
}
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 ParseIntPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const num = Number(value);
if (isNaN(num)) {
const msg1 = `"${metadata.data}" in the path parameters must have the Number data type.`;
throw new TypeError(msg1);
}
return num;
}
}
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'
}
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);
}
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 {}
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;
}
}
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 });
}
}
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.
Posted on October 3, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.