Double click to toggle Read Mode.

NestJS Course

Github Link

Table of Content

  1. Nest from Scratch
  2. With Nest CLI
  3. Validating data with ValidationPipe
  4. Services and Repositories

Nest from Scratch

Initialize project and install dependencies

LibraryDescription
@nestjs/commonContains vast majority of functions, classes, etc, that we need from Nest
@nestjs/coreThe core runtime of NestJS that powers dependency injection, module loading, and lifecycle management
@nestjs/platform-expressLets Nest use Express JS for handling HTTP requests
reflect-metadataHelps make decorators work. Tons more on this in just a minute!
typescriptWe write Nest apps with Typescript.

Initialize typescript - (tsconfig.json)

{ "compilerOptions": { "module": "commonjs", "target": "es2017", "experimentalDecorators": true, "emitDecoratorMetadata": true } }
SettingDescription
"experimentalDecorators": trueEnables the use of decorators (@Something) in TypeScript. Without this, NestJS decorators (like @Controller()) would throw errors.
"emitDecoratorMetadata": trueWorks together with the reflect-metadata library. Emits extra type information about classes and methods at runtime, which NestJS uses for dependency injection.

Common pattern in server

flowchart LR
    A[Request]
    G[Response]
    subgraph Server
        direction LR
        B[**Pipe**<br/><br/>Validate data<br/>contained in<br/>the request]
        B --> C[**Guard**<br/><br/>Make sure<br/>the user is<br/>authenticated]
        C --> D[**Controller**<br/><br/>Route the<br/>request to a<br/>particular<br/>function]
        D --> E[**Service**<br/><br/>Run some<br/>business logic]
        E --> F[**Repository**<br/><br/>Access a<br/>database]
    end

    A --> Server
    direction RL
    Server --> G

    %% Define style classes
    classDef bigText font-size:30px;

    %% Apply them
    class A,B,C,D,E,F,G bigText

NestJS provides More

PartDescription
PipesValidates incoming data
GuardsHandles authentication
ControllersHandles incoming requests
ServicesHandles data access and business logic
RepositoriesHandles data stored in a DB
ModulesGroups together code
FiltersHandles errors that occur during request handling
InterceptorsAdds extra logic to incoming requests or outgoing responses

We need at least module and controller

Create (src/main.ts)

import { Controller, Module, Get } from "@nestjs/common"; import { NestFactory } from "@nestjs/core"; @Controller() class AppController { @Get() getRootRoute() { return "hi there!"; } } @Module({ controllers: [AppController], }) class AppModule {} async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();

Run

npx ts-node-dev src\main.ts

Check at localhost:3000



Refactoring

File Structure

Conventions

Create app.controller.ts

import { Controller, Get } from "@nestjs/common"; @Controller() export class AppController { @Get() getRootRoute() { return "hi there!"; } }

Create app.module.ts

import { Module } from "@nestjs/common"; import { AppController } from "./app.controller"; @Module({ controllers: [AppController], }) export class AppModule {}

Update app.controller.ts

import { NestFactory } from "@nestjs/core"; import { AppModule } from "./app.module"; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();

Adding more Routes

Update app.controller.ts

import { Controller, Get } from "@nestjs/common"; @Controller("/app") export class AppController { @Get("/hi") getRootRoute() { return "hi there!"; } @Get("/bye") getByeThere() { return "bye there!"; } }



With Nest CLI

Install nest cli

npm i -g @nestjs/cli

Create nest project (message)

nest new project_name

Run the project

npm run start:dev

Delete everything in src folder except main

Generate messages module

nest generate module messages

Use message module in main (Remove app module)

import { NestFactory } from "@nestjs/core"; import { MessagesModule } from "./messages/messages.module"; async function bootstrap() { const app = await NestFactory.create(MessagesModule); await app.listen(3000); } bootstrap();

Generate Controller inside messages folder

nest generate controller messages/messages --flat

Update src/messages/messages.controller.ts

import { Controller, Get, Post } from "@nestjs/common"; @Controller("messages") export class MessagesController { @Get() listMessages() {} @Post() createMessage() {} @Get("/:id") getMessage() {} }

Test using Postman or Thunder Client (vscode extension) for success status code

Access Body and Params

import { Controller, Get, Post, Body, Param } from "@nestjs/common"; @Controller("messages") export class MessagesController { @Get() listMessages() {} @Post() createMessage(@Body() body: any) { console.log(body); } @Get("/:id") getMessage(@Param("id") id: string) { console.log(id); } }

Test for console log




Validating data with ValidationPipe

npm install class-validator class-transformer
import { NestFactory } from '@nestjs/core'; + import { ValidationPipe } from '@nestjs/common'; import { MessagesModule } from './messages/messages.module'; async function bootstrap() { const app = await NestFactory.create(MessagesModule); + app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); } bootstrap();

Create class describing properties of request body

export class CreateMessageDto { content: string; }

Add validation rules

+ import { IsString } from 'class-validator'; export class CreateMessageDto { + @IsString() content: string; }

Apply Validation in Controller route handler

How just adding dto type works?

When ts compiles to js. Type information is lost but Because of emitDecoratorMetadata in tsconfig, some of type information is also converted into javascript

import { Controller, Get, Post, Body, Param } from '@nestjs/common'; + import { CreateMessageDto } from './dtos/create-message.dto'; @Controller('messages') export class MessagesController { @Get() listMessages() {} @Post() + createMessage(@Body() body: CreateMessageDto) { console.log(body); } @Get('/:id') getMessage(@Param('id') id: string) { console.log(id); } }

What is happening

Services and Repositories

Services and Repositories end up having similar method names

Create Message Repository

import { readFile, writeFile } from "fs/promises"; export class MessagesRepository { async findOne(id: string) { const contents = await readFile("messages.json", "utf8"); const messages = JSON.parse(contents); return messages[id]; } async findAll() { const contents = await readFile("messages.json", "utf8"); const messages = JSON.parse(contents); return messages; } async create(content: string) { const contents = await readFile("messages.json", "utf8"); const messages = JSON.parse(contents); const id = Math.floor(Math.random() * 999); messages[id] = { id, content }; await writeFile("messages.json", JSON.stringify(messages)); } }

Create Message Service

import { MessagesRepository } from "./messages.repository"; export class MessagesService { messagesRepo: MessagesRepository; constructor() { // Service is creating its own dependencies // DONT DO THIS ON REAL APPS // USE DEPENDENCY INJECTION this.messagesRepo = new MessagesRepository(); } findOne(id: string) { return this.messagesRepo.findOne(id); } findAll() { return this.messagesRepo.findAll(); } create(content: string) { return this.messagesRepo.create(content); } }

Update Message Controller

import { Controller, Get, Post, Body, Param, + NotFoundException, } from "@nestjs/common"; import { CreateMessageDto } from "./dtos/create-message.dto"; + import { MessagesService } from "./messages.service"; @Controller("messages") export class MessagesController { + messagesService: MessagesService; + constructor() { + // DONT DO THIS ON REAL APP + // USE DEPENDENCY INJECTION + this.messagesService = new MessagesService(); + } @Get() listMessages() { + return this.messagesService.findAll(); } @Post() createMessage(@Body() body: CreateMessageDto) { + return this.messagesService.create(body.content); } @Get("/:id") + async getMessage(@Param("id") id: string) { + const message = await this.messagesService.findOne(id); + + if (!message) { + throw new NotFoundException("message not found"); + } + + return message; + } }

Create messages file

{}

Test using Postman or Thunder Client (vscode extension) for success Create and Read.

Inversion of Control Principle

Nest Dependency Injection Container

Refactoring

With Better way

Update Message Repository

import { Injectable } from "@nestjs/common"; @Injectable() export class MessagesRepository { // ... }

Update Message Service

import { Injectable } from "@nestjs/common"; @Injectable() export class MessagesService { constructor(public messagesRepo: MessagesRepository) {} // shorthand // ... }

Update Message Module

// ...imports import { MessagesService } from "./messages.service"; import { MessagesRepository } from "./messages.repository"; @Module({ controllers: [MessagesController], providers: [MessagesService, MessagesRepository], // Add This })

Update Message Controller

// ... @Controller("messages") export class MessagesController { constructor(public messagesService: MessagesService) {} // ... }

Test using Postman or Thunder Client (vscode extension) for success Create and Read.

More on Dependency Injection

nest new di
nest g module computer nest g module cpu nest g module disk nest g module power
nest g service cpu nest g service disk nest g service power
nest g controller computer

Update main.ts

import { NestFactory } from "@nestjs/core"; import { ComputerModule } from "./computer/computer.module"; async function bootstrap() { const app = await NestFactory.create(ComputerModule); await app.listen(process.env.PORT ?? 3000); } bootstrap();

DI within module

Steps: From one service to another service

  1. Add @Injectable() to service1
  2. In module, add service1 in list of providers
  3. In service2 constructor, initialize service1

DI between module

Steps: From service1 of module1 to service2 of module2

  1. Add @Injectable() to service1 (this is already done while generating service)
  2. In module1, add service1 to list of providers (also done) and list of exports (not done)
  3. In module2, import module1 and add module1 in list of imports
  4. In service2 constructor, initialize service1

Importing Power service in CPU service (DI between module)

Create supplyPower method in Power Service

import { Injectable } from "@nestjs/common"; @Injectable() export class PowerService { supplyPower(watts: number) { console.log(`Supplying ${watts} worth of power.`); } }

Add PowerService in list of exports

import { Module } from "@nestjs/common"; import { PowerService } from "./power.service"; @Module({ providers: [PowerService], exports: [PowerService], // add this }) export class PowerModule {}

Import power module in cpu module

import { Module } from "@nestjs/common"; import { CpuService } from "./cpu.service"; import { PowerModule } from "../power/power.module"; // add this @Module({ imports: [PowerModule], // add this providers: [CpuService], }) export class CpuModule {}

Use Power Service

import { Injectable } from "@nestjs/common"; import { PowerService } from "../power/power.service"; // add this @Injectable() export class CpuService { constructor(private powerService: PowerService) {} // add this }

Define compute method

import { Injectable } from "@nestjs/common"; import { PowerService } from "../power/power.service"; @Injectable() export class CpuService { constructor(private powerService: PowerService) {} compute(a: number, b: number) { console.log("Drawing 10 watts of power from Power Service"); this.powerService.supplyPower(10); return a + b; } }

Repeat for Disk module

Import power module in disk module

import { Module } from "@nestjs/common"; import { DiskService } from "./disk.service"; import { PowerModule } from "../power/power.module"; // add this @Module({ imports: [PowerModule], // add this providers: [DiskService], }) export class DiskModule {}

Use Power Service

import { Injectable } from "@nestjs/common"; import { PowerService } from "../power/power.service"; // add this @Injectable() export class DiskService { constructor(private powerService: PowerService) {} // add this }

Define getData method

import { Injectable } from "@nestjs/common"; import { PowerService } from "../power/power.service"; @Injectable() export class DiskService { constructor(private powerService: PowerService) {} getData() { console.log("Drawing 20 watts of power from PowerService"); this.powerService.supplyPower(20); return "data!"; } }

Repeat for Computer module

Add exports

import { Module } from "@nestjs/common"; import { CpuService } from "./cpu.service"; import { PowerModule } from "../power/power.module"; @Module({ imports: [PowerModule], providers: [CpuService], exports: [CpuService], // add this }) export class CpuModule {}


import { Module } from "@nestjs/common"; import { DiskService } from "./disk.service"; import { PowerModule } from "../power/power.module"; @Module({ imports: [PowerModule], providers: [DiskService], exports: [DiskService], // add this }) export class DiskModule {}

Import cpu and disk module in computer module

import { Module } from "@nestjs/common"; import { ComputerController } from "./computer.controller"; import { CpuModule } from "../cpu/cpu.module"; // add this import { DiskModule } from "../disk/disk.module"; // add this @Module({ imports: [CpuModule, DiskModule], // add this controllers: [ComputerController], }) export class ComputerModule {}

Use CPU and Disk Service

import { Controller } from "@nestjs/common"; import { CpuService } from "../cpu/cpu.service"; // add this import { DiskService } from "../disk/disk.service"; // add this @Controller("computer") export class ComputerController { constructor( // add this private cpuService: CpuService, private diskService: DiskService ) {} }

Define get route

import { Controller, Get } from "@nestjs/common"; import { CpuService } from "../cpu/cpu.service"; import { DiskService } from "../disk/disk.service"; @Controller("computer") export class ComputerController { constructor( private cpuService: CpuService, private diskService: DiskService ) {} // add this @Get() run() { return [this.cpuService.compute(1, 2), this.diskService.getData()]; } }