Nest JS Validate Query Paramater and DTOs

Nest JS Validate Query Paramater

https://github.com/tkssharma/blogs/tree/master/nestjs-transform-query-medium-main

I want to show you how to transform and validate HTTP request query parameters in NestJS. As you might know, a query parameter is part of the URL of a website, so that means, it’s always a string. In NestJS and ExpressJS it's actually an object that contains strings as values. But sometimes, we want to cast these string types to something else like numbers, dates, or transforming them to a trimmed string, and so on. so lets see this with examples

We initialize a new NestJS project with its CLI. That might take up to a minute. The CLI script will ask you what package manager you want to use. For this example, I select NPM.

$ nest new play-with-params -p npm

After this command is done you can open your project in your code editor. Since I use Visual Studio Code, I gonna open the project by typing:

$ cd play-with-params
$ code .

My project looks like this in VSCode (Visual Studio Code):

Let’s install some dependencies we going to need.

$ npm i class-validator class-transformer class-sanitizer

Now, let’s start to code. In order to have a clean project structure, we going to create some folders and files, don’t worry, we keep it simple.

$ mkdir src/common && mkdir src/common/helper
$ touch src/common/helper/cast.helper.ts
$ touch src/app.dto.ts

Now it’s time to start to code. First, we need to create some helper functions in our src/common/helper/cast.helper.ts file. src/common/helper/cast.helper.ts

interface ToNumberOptions {
  default?: number;
  min?: number;
  max?: number;
}

export function toLowerCase(value: string): string {
  return value.toLowerCase();
}

export function trim(value: string): string {
  return value.trim();
}

export function toDate(value: string): Date {
  return new Date(value);
}

export function toBoolean(value: string): boolean {
  value = value.toLowerCase();

  return value === 'true' || value === '1' ? true : false;
}

export function toNumber(value: string, opts: ToNumberOptions = {}): number {
  let newValue: number = Number.parseInt(value || String(opts.default), 10);

  if (Number.isNaN(newValue)) {
    newValue = opts.default;
  }

  if (opts.min) {
    if (newValue < opts.min) {
      newValue = opts.min;
    }

    if (newValue > opts.max) {
      newValue = opts.max;
    }
  }

  return newValue;
}

As you can see, most of them are pretty simplified, except toNumber. As you can see, you are able to create even complex helper functions which can certain arguments. Validation

import { Transform } from 'class-transformer';
import { IsBoolean, IsDate, IsNumber, IsNumberString, IsOptional } from 'class-validator';
import { toBoolean, toLowerCase, toNumber, trim, toDate } from './common/helper/cast.helper';

export class QueryDto {
  @Transform(({ value }) => toNumber(value, { default: 1, min: 1 }))
  @IsNumber()
  @IsOptional()
  public page: number = 1;

  @Transform(({ value }) => toBoolean(value))
  @IsBoolean()
  @IsOptional()
  public foo: boolean = false;

  @Transform(({ value }) => trim(value))
  @IsOptional()
  public bar: string;

  @Transform(({ value }) => toLowerCase(value))
  @IsOptional()
  public elon: string;

  @IsNumberString()
  @IsOptional()
  public musk: string;

  @Transform(({ value }) => toDate(value))
  @IsDate()
  @IsOptional()
  public date: Date;
}

Now, let’s create our DTO (Data Transfer Object) in order to validate our query. src/app.dto.ts Controller We are almost done, just two steps left. Let’s add our DTO class to the endpoint of the file src/app.controller.ts.

import { Controller, Get, Query } from '@nestjs/common';
import { QueryDto } from './app.dto';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(@Query() query: QueryDto): QueryDto {
    console.log({ query });

    return query;
  }
}

and finally our Configuration

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

  await app.listen(3000);
}
bootstrap();

Last but not least, we need to add a global pipe in order to be able to transform our incoming request data such as query, body, and parameters. we can add this validation pipe at controller level or app level using

 app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
  • whitelist — removes any property of query, body, and a parameter that is not part of our DTO
  • transform — enables the transformation of our incoming request

lets strat our application and hit this api using curl

$ npm run start:dev

NestJS in Terminal Our application runs on port 3000, so let’s visit:

http://localhost:3000 

http://localhost:3000 Looks good right? In our DTO classes, we added default values to our page and our foo property. But so far we are not transforming anything. Let’s visit:

http://localhost:3000/?page=-1&foo=1&bar=%20bar&elon=Elon&musk=50&date=2022-01-01T12:00:00

As you can see, every single property is getting in some way transformed and validated at the same time. so our properties are

page - 1
foo - true
bar - "bar"
elon -  "elon"
musk - 50
date - "ISO Date TZ" (datestamp)

Lets play with Url and Query Param with DTO

Most of the times we have to design search, sort and filtering system in REST apis lets see how we generally design this These URLs can be designed in many ways.

http://localhost:3000/api/v1/search?search_term=hello&age=90&key=testing
http://localhost:3000/api/v1/search?filter[age]=60&filter[name]=tks&filter[type]=employer
http://localhost:3000/users?sort_by=first_name&order=asc
http://localhost:3000/users?ids=1,2,3,4,5,6

simple search

http://localhost:3000/api/v1/search?search_term=hello&age=90&key=testing

simple filter

http://localhost:3000/api/v1/search?filter[age]=60&filter[name]=tks&filter[type]=employer

simple sort with pagination

http://localhost:3000/users?sort_by=first_name&order=asc

Search with DTO

simple search is just about accessing all query param being passed and access those in controller like search based on multiple param which is easy

with this DTO i am allwong User to pass all these query params and url can be

localhost:3000/api/v1/search?search_term=hello&location=jaipur&location=agra&location=delhi&tag=hello&tag=hello&page=1&limit=500
export class SearchDtoParam {
  @ApiProperty({
    description: 'search_term [name, description, legal_name, email] for search',
    required: false,
  })
  @IsOptional()
  @IsString()
  @MinLength(2)
  public search_term!: string;


  @ApiProperty({
    description: 'locations city/country for search',
    required: false,
    type: [String],
  })
  @IsString({ each: true })
  @IsOptional()
  public locations?: string[];

  @ApiProperty({
    description: 'service tags for search',
    required: false,
    type: [String],
  })
  @IsString({ each: true })
  @IsOptional()
  public tags?: string[];

  @ApiProperty({
    description: 'page number for request',
    required: false,
  })
  @IsOptional()
  @Type(() => Number)
  @IsNumber()
  @Min(1)
  public page?: number;

  @ApiProperty({
    description: 'number of records in a request',
    required: false,
  })
  @IsOptional()
  @Type(() => Number)
  @IsNumber()
  @Min(1)
  public limit?: number;
}

In controller we don't need any special Tranfromer to tranform values

@UsePipes(
  new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }),
)
export class Search {
  constructor(
    private readonly queryBus: QueryBus,
  ) { }

  @Get('/search')
  @ApiTags('search')
  @ApiInternalServerErrorResponse({
    description: INTERNAL_SERVER_ERROR,
  })
  @ApiOperation({
    description:
      'Get data based on search_term -> [name, desc, email] with pagination [page & limit 1,100]',
  })
  @ApiOkResponse({ description: RESULTS_RETURNED })
  @ApiBadRequestResponse({ description: PARAMETERS_FAILED_VALIDATION })
  @UsePipes(ValidationPipe)
  @HttpCode(HttpStatus.OK)
  public async search(
    @User() user: UserMetaData,
    @Query() params: SearchDtoParam,
  ): Promise<IQueryResult> {
    try {
      return await this.queryBus.execute(new GetProfileByQuerySearch(params, user));
    } catch (errors) {
      throw errors;
    }
  }
 }
}

Filter with DTO

Simple filter based on spec looks like this and now we need a way to capture all these query params

http://localhost:3000/api/v1/search?filter[age]=60&filter[name]=tks&filter[type]=employer
http:localhost:3000/api/v1/lists?filter[supplier_id]=uuid&include=permissions

we can create DTO for these cases like

import { applyDecorators } from '@nestjs/common';
import { ApiExtraModels, ApiQuery, getSchemaPath } from '@nestjs/swagger';

export function ApiFilterQuery(fieldName: string, filterDto: any) {
  return applyDecorators(
    ApiExtraModels(filterDto),
    ApiQuery({
      required: false,
      name: fieldName,
      style: 'deepObject',
      explode: true,
      type: 'object',
      schema: {
        $ref: getSchemaPath(filterDto),
      },
    }),
  );
}

export class IncludeDtoParam {
  @ApiProperty({
    description: 'include to add additional data in response like permission',
    required: false,
  })
  @IsOptional()
  @MinLength(2)
  public include?: string;
}

export class CustomerFilterDtoParam {
  @ApiProperty({
    required: false,
  })
  @IsOptional()
  @IsUUID()
  public id?: string;

    @ApiProperty({
    description: 'customer id to fetch list of supplier list',
    required: false,
  })
  @IsOptional()
  @IsUUID()
  public customer_id?: string;
}


  @ApiTags('lists')
  @Get('/')
  @ApiOperation({ description: 'Get supplier lists with or without filter filter[id]=uuid&filter[customer_id]=uuid&include=permission' })
  @UsePipes(ValidationPipe)
  @ApiFilterQuery('filter', CustomerFilterDtoParam)
  @HttpCode(HttpStatus.OK)
  @ApiOkResponse({ description: RESULTS_RETURNED })
  @ApiBadRequestResponse({ description: PARAMETERS_FAILED_VALIDATION })
  public async List(
    @User() user: UserMetaData,
    @Query('filter') filter: CustomerFilterDtoParam,
    @Query() includeDto: IncludeDtoParam) {
    try {
      // TBD
    } catch (errors) {
      throw errors;
    }
  }

Conclusion

There are many possible ways to manages query param in REST APIs, minimum we can try to follow apis specs and follow all guidelines and design accordingly

References

https://www.taniarascia.com/rest-api-sorting-filtering-pagination/ https://devdocs.magento.com/guides/v2.4/rest/retrieve-filtered-responses.html

Comments