Optimizing NestJS pipes for validation and transformation
I am working on enhancing the pipe functionality in my NestJS controller by chaining two pipes. The first pipe will validate the request body against a specific DTO type, while the second pipe will transform this DTO into a specific Type required as an argument for a service.
Current Setup:
@Post()
@UseGuards(JwtAuthGuard, ProductOwnershipGuard)
async create(@Body() createAuctionDto: CreateAuctionDto) {
return this.auctionsService.create(createAuctionDto);
}
Desired Workflow
JwtAuthGuard
verifies if the user is logged inProductOwnershipGuard
ensures that theproduct_id
in the request body belongs to a product owned by the userValidateAuctionPipe
validates the user input against thecreateAuctionDto
typeTransformAuctionPipe
converts thecreateAuctionDto
toAuction
type by incorporating additional properties from the associated product (referenced byproduct_id
)- The
Controller
receives a variable of typeAuction
and passes it to theauctionsService
Attempted Solutions
Initially, I utilized a global ValidationPipe to automatically validate the request body against the CreateAuctionDto
type without any extra annotations.
I experimented with multiple solutions such as:
1.
@Post()
@UseGuards(JwtAuthGuard, ProductOwnershipGuard)
async create(@Body(TransformAuctionPipe) auction: Auction) {
return this.auctionsService.create(createAuctionDto);
}
Unfortunately, the global ValidationPipe checked the body for the Auction type instead of CreateAuctionDto
2.
@Post()
@UseGuards(JwtAuthGuard, ProductOwnershipGuard)
async create(@Body(new ValidationPipe()) createAuctionDto: CreateAuctionDto, @Body(TransformAuctionPipe) auction: Auction) {
return this.auctionsService.create(auction);
}
While this properly validated createAuctionDto, the TransformAuctionPipe received an empty object of Auction type
3.
@Post()
@UseGuards(JwtAuthGuard, ProductOwnershipGuard)
async create(@Body(new ValidationPipe({ expectedType: CreateAuctionDto }), TransformAuctionPipe) auction: Auction) {
return this.auctionsService.create(auction);
}
This approach did not validate the body against CreateAuctionDto at all
- I also attempted using the @UsePipes() decorator instead of passing pipes to @Body() decorator, but it did not yield the desired results
Below are the codes for my pipes:
ValidateDtoPipe
/* eslint-disable @typescript-eslint/ban-types */
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class ValidateDtoPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
TransformAuctionPipe
import { ArgumentMetadata, Injectable, NotFoundException, PipeTransform } from '@nestjs/common';
import { ProductsService } from 'src/api/products/products.service';
import { CreateAuctionDto } from '../dto/create-auction.dto';
import { Auction } from '../entities/auction.entity';
import { EAuctionStatus } from '../types/auction.types';
/**
* This pipe transforms CreateAuctionDto to Auction,
* by fetching associated Product and adding its properties to CreateAuctionDto
* */
@Injectable()
export class TransformAuctionPipe implements PipeTransform<CreateAuctionDto, Promise<Auction>> {
constructor(private readonly productsService: ProductsService) {}
async transform(createAuctionDto: CreateAuctionDto, metadata: ArgumentMetadata): Promise<Auction> {
const product = await this.productsService.findOneById(createAuctionDto.product_id);
if (!product) {
throw new NotFoundException('Product not found.');
}
const auctionStatus: EAuctionStatus = +createAuctionDto.start_at <= Date.now() ? EAuctionStatus.ACTIVE : EAuctionStatus.PENDING;
const { name, description, price } = product;
const auction = new Auction({ ...createAuctionDto, name, description, price, product, status: auctionStatus });
return auction;
}
}