NestJs — A Complete Guide

Amulya Reddy Konda
7 min readMay 1, 2023

This article covers controller, modules, providers, middleware, pipes, guards, interceptors in NestJs.

Getting started

  1. npm i -g @nestjs/cli
  2. nest new project-name

Controller

Skim through this section if you are already familiar with spring boot. Bolded content is different from spring boot.

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('cats')
@HttpCode(204)
// @Redirect('https://nestjs.com', 301) -> Example to redirect
export class CatsController {
@Get(':id')
findAll(@Body() request: RequestDTO, @Param('id') id): string {
return 'This action returns all cats';
}
}
  1. In the above code snippet, @Controller() indicates controller end point. @Get() indicates it is a get call. @Post() indicates post call etc.. @Req() indicates the request body. If you are familiar with spring boot it is very similar to that.
  2. To use path params use @Param('id') annotation. Ex url: http://localhost:3000/<id> . To use query params use @Query('key') annotation. Ex url: http://localhost:3000?key=value
  3. We can have routes with wildcards. Ex. @Get('ab*cd') will match abcd, ab_cd, abecd, and so on. The characters ?, +, *, and () may be used in a route path, and are subsets of their regular expression counterparts. The hyphen ( -) and the dot (.) are interpreted literally by string-based paths.
  4. We can add @HttpCode(204) to override the default 200 status
  5. We can redirect using @Redirect('https://nestjs.com', 301)
  6. Promises and Observables can also be return by NestJs
@Get()
async findAll(): Promise<any[]> { // -> Return a Promise
return [];
}

@Get()
findAll(): Observable<any[]> { // -> Return an RxJS Observable
return of([]);
}

7. Controllers always belong to a module, which is why we include the controllers array within the @Module() decorator. Let’s now see how modules work.

Modules

Skim through this section if you are already familiar with Angular.

  1. A module consists of controllers, providers, imports and exports.
  2. providers— The providers that will be instantiated by the Nest injector and that may be shared at least across this module
  3. controllers— The set of controllers defined in this module which have to be instantiated
  4. imports — The list of imported modules that export the providers which are required in this module
  5. exports — The subset of providers that are provided by this module and should be available in other modules which import this module. You can use either the provider itself or just its token (provide value)

2. To represent the above structure in Modules,

import { Module } from '@nestjs/common';

@Module({
imports: [ProductModule, OrdersModule],
})
export class AppModule {}
import { Module } from '@nestjs/common';
import { ProductController } from './product.controller';
import { ProductService } from './product.service';

@Module({
imports: [FeatureModule1, FeatureModule2],
controllers: [ProductController],
providers: [ProductService],
})
export class ProductModule {}
import { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';

@Module({
imports: [FeatureModule3],
controllers: [OrdersController],
providers: [OrdersService],
exports: [OrdersService],
})
export class OrdersModule {}
import { Module } from '@nestjs/common';
import { FeatureController } from './feature.controller';
import { FeatureService } from './feature.service';

@Module({
controllers: [FeatureController],
providers: [FeatureService],
})
export class FeatureModule1 {}

Feature2 and Feature3 Modules in the diagram are written similar to FeatureModule1.

3. Shared Modules — If we want to share an instance of the OrdersService between several other modules, we need to export the OrdersService provider by adding it to the module's exports array, as shown above.

Providers

  1. Providers are the services that can be injected as a dependency.
import { Injectable } from '@nestjs/common';
import { Product } from './interfaces/product.interface';

@Injectable()
export class ProductService {
private readonly products: Product[] = [];

findAll(): Product[] {
return this.products;
}
}

Constructor based injection:

2. @Injectable() is used to identify the class as injectable (service or repository or factories or helpers).

3. To generate the service via command line: nest g service product

4. Call this service from controller through depenedency injection

@Controller('cats')
export class ProductController {
constructor(private productService: ProductService) {}
...
}

5. We can use @Optional() if none of the dependency is passed, the default values should be used.

// constructor-based injection
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}

Property based injection:

6. If your top-level class depends on either one or multiple providers, passing them all the way up by calling super() in sub-classes from the constructor can be very tedious.

If your class doesn’t extend another provider, you should always prefer using constructor-based injection.

// property based injection
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}

7. Providers must be added to Module as shown in the above section

Middleware

  1. Middleware is a function which is called before the route handler. Middleware functions have access to the request and response objects, and the next() middleware function in the application’s request-response cycle.
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}

2. Annotate a class with @Injectable() and implement NestMiddleware

3. Apply the middleware in the module as shown below:

export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'item', method: RequestMethod.GET },
{ path: 'order', method: RequestMethod.POST },
'cart/(.*)',
)
.forRoutes({ path: 'product', method: RequestMethod.GET });
// forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
}
}

4. Functional Middleware:

import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`Request...`);
next();
};

5. Multiple middlewares:

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

6. Global middleware:

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

7. Middleware functions can perform the following tasks:

  • execute any code.
  • make changes to the request and the response objects.
  • end the request-response cycle.
  • call the next middleware function in the stack.
  • if the current middleware function does not end the request-response cycle, it must call next() to pass control to the next middleware function. Otherwise, the request will be left hanging.

Pipes

A pipe is a class annotated with the @Injectable() decorator, which implements the PipeTransform interface.

@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}

Class Validator and Class Transformer:

npm i --save class-validator class-transformer
import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
@IsString()
name: string;

@IsInt()
age: number;

@IsString()
breed: string;
}

Creating Custom Pipe

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';

@Injectable()
export class ValidationPipe 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);
}
}

Global scoped pipe

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();

Guards

  1. A guard is a class annotated with the @Injectable() decorator, which implements the CanActivate interface.
  2. Guards have a single responsibility. They determine whether a given request will be handled by the route handler or not, depending on certain conditions. This is often referred to as authorization. Authentication is typically handled by Middleware.

3. Authorization guard / AuthGuard

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}

4. Binding guards

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

5. Add guards to Module

@Module({
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
})
export class AppModule {}

Interceptors

  1. An interceptor is a class annotated with the @Injectable() decorator and implements the NestInterceptor interface.
  2. Each interceptor implements the intercept() method, which takes two arguments. The first one is the ExecutionContext instance (exactly the same object as for guards). The ExecutionContext inherits from ArgumentsHost. The second argument is a CallHandler
@UseInterceptors(LoggingInterceptor)
export class ProductController {}
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');

const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}

3. Execution context

By extending ArgumentsHost, ExecutionContext also adds several new helper methods that provide additional details about the current execution process.

4. Call handler

The CallHandler interface implements the handle() method, which you can use to invoke the route handler method at some point in your interceptor. If you don't call the handle() method in your implementation of the intercept() method, the route handler method won't be executed at all.

5. Add the interceptor to module

@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}

6. Interceptors have a set of useful capabilities:

  • bind extra logic before / after method execution
  • transform the result returned from a function
  • transform the exception thrown from a function
  • extend the basic function behavior
  • completely override a function depending on specific conditions (e.g., for caching purposes)

--

--