Create a new NestJS project using the CLI:
nest new project-nameThis command creates a new NestJS project with all the necessary boilerplate code and dependencies.
Routes in NestJS are defined using decorators in controller files. Here's how to set up basic routes:
@Controller('user') // Defines the base route prefix
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('all') // GET /user/all
getHello(): string {
return this.appService.getHello();
}
}Handle dynamic parameters in routes using the @Param decorator:
@Get(':id') // GET /user/:id
findOne(@Param('id') id: string) {
return `This action returns a property ${id}`;
}There are two ways to handle multiple route parameters:
- Accessing all parameters as an object:
@Get(':id/:slug')
findOne(@Param() params) {
// Returns object: { id: '1', slug: 'property-1' }
return params;
}- Accessing specific parameters individually:
@Get(':id/:slug')
findOne(@Param('id') id: string, @Param('slug') slug: string) {
return `id: ${id}, slug: ${slug}`;
}NestJS provides several CLI commands to generate project components:
# Generate a new module
nest g module module-name
# Generate a new controller
nest g co controller-name
# Other common commands:
# nest g s # Generate a service
# nest g pi # Generate a pipe
# nest g mi # Generate a middlewareHandle POST request bodies using the @Body decorator:
- Access entire request body:
@Post()
create(@Body() body) {
return body;
}- Access specific body properties:
@Post()
create(@Body('name') name: string) {
return name;
}Customize HTTP response status codes using the @HttpCode decorator:
@Post()
@HttpCode(202) // Sets response status code to 202 Accepted
create(@Body('name') name: string) {
return name;
}Common HTTP status codes:
- 200: OK (default for GET requests)
- 201: Created (default for POST requests)
- 202: Accepted
- 204: No Content
- 400: Bad Request
- 401: Unauthorized
- 403: Forbidden
- 404: Not Found
- 500: Internal Server Error
NestJS provides powerful pipes for data transformation and validation. Pipes can transform input data to the desired format and validate it before it reaches the route handler.
- ParseIntPipe: Automatically transforms string values to integers
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
console.log(typeof id); // 'number'
return `This action returns a property ${id}`;
}- ParseFloatPipe: Transforms string values to floating-point numbers
@Get(':price')
findByPrice(@Param('price', ParseFloatPipe) price: number) {
return `Items with price ${price}`;
}- ParseBoolPipe: Transforms string values to booleans
@Get(':isActive')
findActive(@Param('isActive', ParseBoolPipe) isActive: boolean) {
return `Active status: ${isActive}`;
}- ParseArrayPipe: Transforms array-like string values to arrays
@Post()
createMany(@Body(new ParseArrayPipe({ items: Number })) items: number[]) {
return items;
}- For search Queries:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id, @Query('sort', ParseBoolPipe) sort) {
console.log(typeof id); // number
console.log(typeof sort); // boolean
return `This action returns a property ${id}`;
}You can create custom pipes for specific transformation needs:
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class CustomTransformPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
// Your transformation logic here
return transformedValue;
}
}Usage:
@Get(':id')
findOne(@Param('id', CustomTransformPipe) id) {
return `Transformed value: ${id}`;
}Install the dependenies
npm i --save class-validator class-transformer// In /property/dto/createProperty.dto.ts
import { IsInt, IsString } from 'class-validator';
export class CreatePropertyDto {
@IsString()
name: string;
@IsString()
description: string;
@IsInt()
area: number;
}
To remove any extra fields that are not defined in the CreatePropertyDto
@Post()
@UsePipes(new ValidationPipe({ whitelist: true })) //whitelist: true will remove any extra fields that are not defined in the CreatePropertyDto
create(@Body() body: CreatePropertyDto) {
return body;
}To throw an error if any extra fields are present in the request body
@Post()
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) //whitelist: true will remove any extra fields that are not defined in the CreatePropertyDto\n//forbidNonWhitelisted: true will throw an error if any extra fields are present in the request body
create(@Body() body: CreatePropertyDto) {
return body;
}Another way to do it
@Post()
create(
@Body(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) // another way of doing it
body: CreatePropertyDto,
) {
return body;
}- For custom error message and more validation
import { IsInt, IsPositive, IsString, Length } from 'class-validator';
export class CreatePropertyDto {
@IsString()
@Length(2, 20, { message: 'Name is too short or too long' })
name: string;
@IsString()
description: string;
@IsInt()
@IsPositive()
area: number;
}How to make groups so that we can use the same but slightly changed validation for create and update
@Post()
create(
@Body(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
groups: ['create'],
always: true,
}),
)
body: CreatePropertyDto,
) {
return body;
}
@Patch(':id')
update(
@Body(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
groups: ['update'],
always: true,
}),
)
body: CreatePropertyDto,
) {
return body;
}
// Then in dto
import { IsInt, IsPositive, IsString, Length } from 'class-validator';
export class CreatePropertyDto {
@IsString()
@Length(2, 20, { message: 'Name is too short or too long' })
name: string;
@IsString()
@Length(2, 10, { groups: ['create'] })
@Length(1, 15, { groups: ['update'] })
description: string;
@IsInt()
@IsPositive()
area: number;
}// In the main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
}),
);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
// So after we add global validation in this we can remove the validation form endpoints in the controller
@Patch(':id')
update(
@Body(
// new ValidationPipe({
// whitelist: true,
// forbidNonWhitelisted: true,
// groups: ['update'],
// always: true,
// }),
)
body: CreatePropertyDto,
) {
return body;
}
Move the useGlobalPipes from main.ts and use it as a provider in the module like this as shown below
import { Module, ValidationPipe } from '@nestjs/common';
import { PropertyController } from './property.controller';
import { APP_PIPE } from '@nestjs/core';
@Module({
controllers: [PropertyController],
providers: [
{
provide: APP_PIPE,
// useClass: ValidationPipe, // Global validation without any options -- useClass
// Global validation with options -- useValue
useValue: new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
}),
},
],
})
export class PropertyModule {}Transform the incoming request data to the desired type and enable the implicit conversion of the incoming request data to the desired type
import { Module, ValidationPipe } from '@nestjs/common';
import { PropertyController } from './property.controller';
import { APP_PIPE } from '@nestjs/core';
@Module({
controllers: [PropertyController],
providers: [
{
provide: APP_PIPE,
// useClass: ValidationPipe, // Global validation without any options -- useClass
// Global validation with options -- useValue
useValue: new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true, // This will transform the incoming request data to the desired type
transformOptions: { enableImplicitConversion: true }, // This will enable the implicit conversion of the incoming request data to the desired type
}),
},
],
})
export class PropertyModule {} // make a dto for the param along with the validation
import { IsInt, IsPositive } from 'class-validator';
export class idParamDto {
@IsInt()
@IsPositive()
id: number;
}
// Then type it in the endpoint like so
@Patch(':id')
update(
@Param() param: idParamDto,
@Body()
body: CreatePropertyDto,
) {
return body;
}
import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
@Injectable() // This is a provider that can be injected into other components -- to use this outside of the module
export class ParseIdPipe implements PipeTransform<string, number> {
transform(value: string): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Id must be a number');
}
if (val <= 0) {
throw new BadRequestException('Id must be a positive number');
}
return val;
}
}
// then use it like this
@Patch(':id')
update(
@Param('id', ParseIdPipe) id,
@Body()
body: CreatePropertyDto,
) {
return body;
}// To use zod for validation, we don't need to define the provider for global validation in the module
import { Module } from '@nestjs/common';
import { PropertyController } from './property.controller';
// import { APP_PIPE } from '@nestjs/core';
@Module({
controllers: [PropertyController],
// providers: [
// {
// provide: APP_PIPE,
// // useClass: ValidationPipe, // Global validation without any options -- useClass
// // Global validation with options -- useValue
// useValue: new ValidationPipe({
// whitelist: true,
// forbidNonWhitelisted: true,
// transform: true, // This will transform the incoming request data to the desired type
// transformOptions: { enableImplicitConversion: true }, // This will enable the implicit conversion of the incoming request data to the desired type
// }),
// },
// ],
})
export class PropertyModule {}
//Create a zod dto -- createPropertyZod.dto.ts
import { z } from 'zod';
export const createPropertySchema = z
.object({
name: z.string(),
description: z.string().min(5),
area: z.number().positive(),
})
.required();
export type CreatePropertyZodDto = z.infer<typeof createPropertySchema>;
// then we can create a zodValidationPipeline.ts
import { BadRequestException, PipeTransform } from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: any) {
const parsedValue = this.schema.safeParse(value);
if (parsedValue.success) return parsedValue.data;
throw new BadRequestException(parsedValue.error.format());
}
}
// then just use it in endpoint in the controller
@Post()
@UsePipes(new ZodValidationPipe(createPropertySchema))
create(
@Body()
body: CreatePropertyZodDto,
) {
return body;
}// Just use this in the endpoint
@Patch(':id')
update(
@Param('id', ParseIdPipe) id,
@Body()
body: CreatePropertyDto,
@Headers('host') header,
) {
return header;
}// make a headers dto
import { Expose } from 'class-transformer';
import { IsString } from 'class-validator';
export class HeadersDto {
@IsString()
@Expose({ name: 'access-token' })
accessToken: string;
}
// then use it in the endpoint
@Patch(':id')
update(
@Param('id', ParseIdPipe) id,
@Body()
body: CreatePropertyDto,
@Headers('host') header: HeadersDto,
) {
return header;
}
// but it will not work for the headers, so we need to create a custom decorator for validation
// so to create a custom decorator
// create a request-header in pipes
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validateOrReject } from 'class-validator';
export const RequestHeader = createParamDecorator(
async (targetDto: any, ctx: ExecutionContext) => {
const headers = ctx.switchToHttp().getRequest().headers;
const dto = plainToInstance(targetDto, headers, {
excludeExtraneousValues: true,
});
await validateOrReject(dto);
return dto;
},
);
// then use it like this
@Patch(':id')
update(
@Param('id', ParseIdPipe) id,
@Body()
body: CreatePropertyDto,
@RequestHeader(new ValidationPipe({ validateCustomDecorators: true }))
header: HeadersDto,
) {
return header;
}- Normally a class would create it's own dependencies but with Inversion Of Control an external entity like a framework takes the control and create the dependencies for the classes.
//Dependency injection can be implemented like this
@Controller('property')
export class PropertyController {
properyService: PropertyService;
constructor(propertyService: PropertyService) {
// Don't create your dependencies like this in a real application, instead use dependency injection
// this.properyService = new PropertyService();
this.properyService = propertyService; // This is how we can use dependency injection
}
//SO this will benefit us to do this
interface Service {
findAll();
findOne();
create();
update();
}
@Controller('property')
export class PropertyController {
properyService: PropertyService;
constructor(propertyService: Service) {
// Don't create your dependencies like this in a real application, instead use dependency injection
// this.properyService = new PropertyService();
this.properyService = propertyService; // This is how we can use dependency injection
}
@Controller('property')
export class PropertyController {
constructor(private propertyService: PropertyService) {
// Don't create your dependencies like this in a real application, instead use dependency injection
// this.properyService = new PropertyService();
}
@Get()
findAll() {
return this.propertyService.findAll();
}Installation
npm i --save @nestjs/typeorm typeorm mysql2
// for postgres
npm i --save @nestjs/typeorm typeorm pgSetting up TypeORM
//After installation, in app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PropertyModule } from './property/property.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { pgConfig } from 'dbConfig';
@Module({
imports: [PropertyModule, TypeOrmModule.forRoot(pgConfig)], // Add TypeOrmModule here along with the config
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
// For the config, in the root directory, make a dbConfig.ts file
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
export const pgConfig: PostgresConnectionOptions = {
url: 'your db url',
type: 'postgres',
port: 3306,
entities: [],
synchronize: true, // This will automatically create the tables in the database // For development only, use false for production
};-
Make an entities dir in src directory
-
Then in it make a property.entity.ts file
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Propery {
// @PrimaryColumn() // This will make this a primary key
@PrimaryGeneratedColumn() // This will automatically generate a unique id for each property and is a primary key
id: number;
@Column()
name: string;
@Column()
description: string;
@Column({ default: 0 })
price: number;
}- Then add the enity in dbConfig.ts
import { Property } from 'src/entities/property.entity';
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
export const pgConfig: PostgresConnectionOptions = {
url: 'your db url',
type: 'postgres',
port: 3306,
entities: [Property],
synchronize: true, // This will automatically create the tables in the database // For development only, use false for production
};- Then run the dev server
npm run start:dev
// After this, this will create a schema in the database you are using, for instance if I'm using neon, this will show the new schema named propery in the tables tabimport { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
export const pgConfig: PostgresConnectionOptions = {
url: 'your db url',
type: 'postgres',
port: 3306,
entities: [__dirname + '/**/*.entity{.ts,.js}'], // This will automatically load all the entities from the entities folder
synchronize: true, // This will automatically create the tables in the database // For development only, use false for production
};It offers two ways to do crud operations
- Query Builder
- Repository Pattern
-
From Repository Pattern
-
To use the repository class, we need to register it in the module
import { Module, ValidationPipe } from '@nestjs/common';
import { PropertyController } from './property.controller';
import { APP_PIPE } from '@nestjs/core';
import { PropertyService } from './property.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Property } from 'src/entities/property.entity';
@Module({
imports: [TypeOrmModule.forFeature([Property])], // This will import the Property entity into the PropertyModule
controllers: [PropertyController],
providers: [
{
provide: APP_PIPE,
// useClass: ValidationPipe, // Global validation without any options -- useClass
// Global validation with options -- useValue
useValue: new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true, // This will transform the incoming request data to the desired type
transformOptions: { enableImplicitConversion: true }, // This will enable the implicit conversion of the incoming request data to the desired type
}),
},
PropertyService,
],
})
export class PropertyModule {}- In the service, we inject the repository of our entity
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Property } from 'src/entities/property.entity';
import { Repository } from 'typeorm';
import { CreatePropertyDto } from './dto/createProperty.dto';
@Injectable()
export class PropertyService {
constructor(
@InjectRepository(Property)
private propertyRepository: Repository<Property>,
) {} // here
async findOne() {}
async findAll() {}
async create(dto: CreatePropertyDto) {
return await this.propertyRepository.save(dto); // then here we call the repo.save function // this will insert a new record in the table
}
async update() {}
async delete() {}
}npm i @nestjs/mapped-types- Always specify types for parameters and return values
- Use meaningful route names that follow REST conventions
- Group related functionality into modules
- Use services for business logic
- Implement error handling using exception filters
- Use DTOs (Data Transfer Objects) for request validation
- Use appropriate pipes for data transformation and validation
- Implement custom pipes for specific business logic needs