Nest JS Validate Query Paramater and DTOs
Nest JS Validate Query Paramater
Github Link
github.com/tkssharma/blogs/tree/master/nest..
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
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
taniarascia.com/rest-api-sorting-filtering-.. devdocs.magento.com/guides/v2.4/rest/retrie..