Core NestJS Concepts You Need to Know

Updated: December 31, 2023 By: Guest Contributor Post a comment

Introduction

NestJS is a progressive Node.js framework for building efficient and scalable server-side applications. It leverages TypeScript and combines elements of Object-Oriented Programming (OOP), Functional Programming (FP), and Functional Reactive Programming (FRP). By using a modular approach, decorators, and dependency injection, NestJS introduces a level of structure that’s suited for both small-scale projects and large enterprise applications. This guide covers the core concepts you need to master NestJS development.

Modules

In NestJS, everything starts with modules. They are a way of organizing code within an application and encapsulating providers which can be shared across the application. Let’s define a basic module with a single controller.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';

@Module({
  controllers: [AppController],
})
export class AppModule {}

Controllers

Controllers handle incoming requests and return responses. They are decorated with @Controller(), and the routes are configured with decorators such as @Get(), @Post(), etc.

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

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

Providers and Services

Providers can be injected into controllers or other providers, encapsulating the business logic. Services are common providers and are usually decorated with @Injectable().

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

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

Dependency Injection

NestJS makes heavy use of dependency injection which is key to creating scalable systems. Services can be injected into controllers by simply adding a constructor parameter with a type.

import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) { }

  @Get()
  findAll() {
    return this.catsService.findAll();
  }
}

Pipes

Pipes are NestJS components that can transform or validate request data. Use them as an argument decorator, such as @Body() combined with @Post() for data transformation and validation.

import { Body, Controller, Post } from '@nestjs/common';
import { CreateCatDto } from './create-cat.dto';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) { }

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }
}

Guards

Guards are responsible for whether a certain request will be handled by the route handler or not, often used for authentication.

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

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

Interceptors

Interceptors allow you to intercept the request/response stream, adding extra logic like logging, transformation, or response mapping.

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable {
    console.log('Before...');

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

Decorators

Custom decorators in NestJS allow you to define and attach new behavior to method parameters or class properties. They are widely used to enrich the context information, like extracting a user id from an authorization token.

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const GetUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

Microservices

NestJS supports building scalable microservices with different transport layers like TCP, Redis, MQTT, and more.

import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class MathController {
  @MessagePattern('add')
  add(data: number[]): number {
    return (data || []).reduce((a, b) => a + b);
  }
}

WebSocket

Real-time bi-directional communication is easy with NestJS using WebSocket gateways.

import { SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server } from 'socket.io';

@WebSocketGateway()
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('message')
  handleMessage(client: any, payload: any): void {
    this.server.emit('message', payload);
  }
}

Conclusion

This guide touched upon the core concepts of NestJS: modules, controllers, providers, dependency injection, pipes, guards, interceptors, decorators, microservices, and websockets. Armed with these fundamentals, you’re well-equipped to delve deeper into NestJS and harness its power to build efficient and scalable server-side solutions.