Nest.js

[Nest.js] 8 - CRUD summary code(#1 ~ #7)

ESTJames 2021. 9. 10. 18:57

Life Cycle

  1. Nest Application
  2. Reqeust
  3. Middleware - Global bound
  4. Middleware - Module bound
  5. Guard - Global
  6. Guard - Controller
  7. Guard - Controller, Root
  8. Interceptor - Global
  9. Interceptor - Controller
  10. Interceptor - Route
  11. Pipe - Global
  12. Pipe - Controller
  13. Pipe - Route
  14. Pipe - Parameter
  15. Controller - Method Handler, DTOs
  16. Service -  Business logic
  17. Repository - TypeORM, Entity
  18. Interceptor -  Route
  19. Interceptor - Controller
  20. Interceptor - Global
  21. filter - Handling Exceptions 
  22. Response

- Bold indicates what used in this sample project


1. Nest Application

 

- src/main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule); // 1

  app.useGlobalPipes( 
    new ValidationPipe({ 
      whitelist: true, 
      forbidNonWhitelisted: true, 
      transform: true,
    }),
  );

  await app.listen(3000);
}
bootstrap(); // 2

// 1. await NestFactory.create(AppModule) : register a root module, AppModule

// 2. bootstrap() : run application

 

- src/app.module.ts

import { Module } from '@nestjs/common';
import { BoardsModule } from './boards/boards.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeORMConfig } from './configs/typeorm.config';

@Module({
  imports: [
    TypeOrmModule.forRoot(TypeORMConfig), // 1
    BoardsModule // 2
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

// 1. TypeOrmModule.forRoot(TypeORMConfig) : Register TypeORM configuration(see below #6.2)

// 2. Register other module at this Root module(there is one module in this chapter.)

 

- src/boards/boards.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardRepository } from './board.repository';
import { BoardsController } from './boards.controller';
import { BoardsService } from './boards.service';

@Module({
  imports: [TypeOrmModule.forFeature([BoardRepository])], // 1
  controllers: [BoardsController],  // 2
  providers: [BoardsService],  //3
})
export class BoardsModule {}

// 1. TypeOrmModule.forFeature([BoardRepository]) : register the repository in its Module (see below #6)

// 2. register its controller in its module

// 3. register its service in its module


2. Request

- REST API

GET /boards

GET /boards/:id

POST /boards

PUT /boards/:id

DELETE /boards/:id

- Request and Response With Postman for now
   , will update those documents with Swagger


3. Pipes

3.1 Global Pipe

- src/main.ts

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( // 1
    new ValidationPipe({ // 2
      whitelist: true, // 3 
      forbidNonWhitelisted: true, // 4 
      transform: true, // 5
    }),
  );

  await app.listen(3000);
}
bootstrap();

// 1. app.useGlobalPipes(...) - register a pipe in Global-level

// 2. new ValidationPipe({...}) - use a built-in pipe, ValidationPipe

// 3. whitelist: true - fillter out properties that should not be received by the method handler.
                               - e.g., a handler expect "email" and "password" properties
                                 , but a request also includes an "age" property(called non-whitelisted property), 
                                  this property can be automatically removed from the resulting DTO

// 4. forbidNonWhiteListed: true - making stop the request from processing when non-whtelisted are present,
                                                         and return an error response to the user.
                                                         To enable this, set the forbidNonWhitelisted option to true,
                                                         in combination with setting whitelist to true

// 5. transform: true - perform conversion of primitive types.
                                  - e.g., request type string -> parameter type number

 

3.2 Parameter Pipe

- src/boards/pipes/board-status-validation.pipe.ts

import { BadRequestException, PipeTransform } from '@nestjs/common';
import { BoardStatus } from '../board-status.enum'; // 1

export class BoardStatusValidationPipe implements PipeTransform {
  readonly StatusOptions = [BoardStatus.PRIVATE, BoardStatus.PUBLIC]; // 1

  transform(value: any) {
    value = value.toUpperCase(); // 2

    if (!this.isStatusValid(value)) { // 3
      throw new BadRequestException( // 4
        `${value} isn't in the status options, which one of PRIVATE or PUBLIC`,
      );
    }

    return value; // 5 
  }

  private isStatusValid(status: any) { // 3
    const index = this.StatusOptions.indexOf(status);
    return index !== -1;
  }
}

// 1. src/boards/board-status.enum.ts 

export enum BoardStatus {
  PRIVATE = 'PRIVATE',
  PUBLIC = 'PUBLIC',
}

// 2. toUpperCase() - accept case-insensitive value 

// 3. validate whether the value matches one of options in enum class

// 4. trow new BadRequestException(...) - if not matched, pipe causes 400 error response to the client

// 5. if matched, pipe sends the value to the next


4. Controller

- src/boards/boards.controller.ts

import { Body, Controller, Delete, Get, Param, Post, Put,} from '@nestjs/common';
import { BoardStatus } from './board-status.enum';
import { Board } from './board.entity';
import { BoardsService } from './boards.service';
import { CreateBoardDto } from './dto/create-board.dto';
import { UpdateBoardDto } from './dto/update-board.dto';
import { BoardStatusValidationPipe } from './pipes/board-status-validation.pipe';

@Controller('boards')
export class BoardsController {
  constructor(private boardsService: BoardsService) {} // 1

  @Get('/:id')
  getBoardById(@Param('id') id: number): Promise<Board> {
    return this.boardsService.getBoardById(id);
  }

  @Get()
  getAllBoards(): Promise<Board[]> {
    return this.boardsService.getAllBoards();
  }

  @Post()
  createBoard(
    @Body('status', BoardStatusValidationPipe) status: BoardStatus, // 2
    @Body() createBoardDto: CreateBoardDto, // 4.1
  ): Promise<Board> {
    return this.boardsService.createBoard(createBoardDto);
  }

  @Put('/:id')
  updateBoard(
    @Param('id') id: number,
    @Body() updateBoardDto: UpdateBoardDto, // 4.2
  ): Promise<Board> {
    return this.boardsService.updateBoard(id, updateBoardDto);
  }

  @Delete('/:id')
  deleteBoard(@Param('id') id: number): Promise<void> {
    return this.boardsService.deleteBoard(id);
  }
}

// 1. Dependency Injection in Constructor

// 2. Bind the Custom pipe in Parameter-Level to check "status" value

 

4.1 create-dto

- src/boars/dto/create-board.dto.ts

import { IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
import { BoardStatus } from '../board-status.enum';

export class CreateBoardDto {
  @IsNotEmpty() // 1
  title: string;

  @IsNotEmpty()
  description: string;

  @IsOptional() // 1 
  @IsEnum(BoardStatus) // 2
  readonly status: BoardStatus = BoardStatus.PUBLIC; // 3
}

// 1. Decorator-based validation - more decorators click the link (http://github.com/typestack/class-validator#usage)

// 2. @IsEnum(BoardStatus) - accept a value in one of enum options.

// 3. initialize a default value when it is not provided from client.

 

4.2. update-dto : Partial Mapped type 

- src/boars/dto/update-board.dto.ts

import { PartialType } from '@nestjs/mapped-types';
import { CreateBoardDto } from './create-board.dto'; // 1

export class UpdateBoardDto extends PartialType(CreateBoardDto) {} // 2

// 1. import which dto is used as base class

// 2. use PartialType to make all properties as optional in "UpdateBoardDto"
       - @IsNotEmpty() is not provided into "Partial" Mapped-Type, which is update-board.dto.ts
       - @IsEnum(BoardStatus) is provided into "status" property in update-dto.ts 
       - more types click the link(https://docs.nestjs.com/openapi/mapped-types)

 


5. Service

- src/boards/boards.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { BoardStatus } from './board-status.enum';
import { Board } from './board.entity'; 				// 6.1
import { BoardRepository } from './board.repository';
import { CreateBoardDto } from './dto/create-board.dto';
import { UpdateBoardDto } from './dto/update-board.dto';

@Injectable()  // 1
export class BoardsService {
  constructor(
    @InjectRepository(BoardRepository)  // 2
    private boardRepository: BoardRepository,
  ) {}

  // READ
  async getBoardById(id: number): Promise<Board> {  // 3
    const found = await this.boardRepository.findOne(id);  

    if (!found) {
      throw new NotFoundException(`can't find Board with id ${id}`);
    }

    return found;
  }

  // READ
  async getAllBoards(): Promise<Board[]> {
    const boards = await this.boardRepository.find();

    if (!boards) {
      throw new NotFoundException(`There is no boards in DB`);
    }

    return boards;
  }

  // CREATE
  async createBoard(createBoardDto: CreateBoardDto): Promise<Board> {
    const { title, description, status } = createBoardDto;

    const board = this.boardRepository.create({  // 4
      title,
      description,
      status,
    });

    await this.boardRepository.save(board);
    return board;
  }

  // UPDATE
  async updateBoard(
    id: number,
    updateBoardDto: UpdateBoardDto,
  ): Promise<Board> {
    const board = await this.getBoardById(id);

    board.title = updateBoardDto.title;
    board.description = updateBoardDto.description;
    board.status = updateBoardDto.status;

    await this.boardRepository.save(board);
    return board;
  }

  // DELETE
  async deleteBoard(id: number): Promise<void> {
    const board = await this.boardRepository.delete(id);

    console.log(board);
  }
}

// 1. @Injectable() :  this class, Provider, can be managed by the Nest IoC container

// 2. @InjectRepository(BoardRepository) : inject the BoardRepository into the BoardsService

// 3. async/ await : allows us to write asynchronous code in more synchronous and readable way.
       Promise : an object is handled in asynchronous processing and represents a task. 
      -> click link for more detail(https://kamilmysliwiec.com/typescript-2-1-introduction-async-await/)

// 4. this.boardRepository.create({...}); : create an entity which is registered in the called repository.
                                                                   -> the same as "const board = new Board(...);


6. Repository

- src/boards/board.repository.ts

import { EntityRepository, Repository } from 'typeorm';
import { Board } from './board.entity';

@EntityRepository(Board) // 1
export class BoardRepository extends Repository<Board> {} // 2

// 1. @EntityRepository(Board) : Used to declare a class as a Custom Repository.
                                                      Custom repository can manage some specific entity or just be generic.
                                                     , optionally can extend AbstractRepository, Repository or TreeRepository

// 2. ... extends Repository<Board> : work with "Board"  entity, supports basic query methods.
       -> more methods and details (https://typeorm.delightful.studio/classes/_repository_repository_.repository.html)

 

- register the repository in its Module

...
import { BoardRepository } from './board.repository';

@Module({
  imports: [TypeOrmModule.forFeature([BoardRepository])],
  ...
})
export class BoardsModule {}

 

6.1 Entity

- src/boards/board.entity.ts

import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { BoardStatus } from './board-status.enum';

@Entity() // 1
export class Board extends BaseEntity { // 
  @PrimaryGeneratedColumn() // 
  id: number;

  @Column() // 
  title: string;

  @Column()
  description: string;

  @Column()
  status: BoardStatus;
}

// 1. @Entity() :  is a class that maps to a database table

board table in mysql

 

6.2 TypeORM 

- src/configs/typeorm.config.ts

import { TypeOrmModuleOptions } from '@nestjs/typeorm';

export const TypeORMConfig: TypeOrmModuleOptions = {
  type: 'mysql',
  host: 'localhost',
  port: 3306,
  username: 'root',
  password: '1234',
  database: 'test',
  entities: [__dirname + '/../**/*.entity.{js,ts}'], // 1
  synchronize: true, // 2
};

// 1. Register entities by file path and name(Using Team Naming Convention)
      -> if needed to regist specific entities only, use below.

...
import { Board } from 'src/boards/board.entity';

export const TypeORMConfig: TypeOrmModuleOptions = {
  ...
  entities: [Board],
  ...
};

7. Response

CRUD( => Create)

- STATUS 201 : CREATE without optional value "status", which is set to the default value "PUBLIC"

 

- STATUS 201   CREATE with "status"

 

400 ERROR : NOT CREATE because of @IsNotEmpty() for title and description

 

- 400 ERROR : NOT CREATE because of @IsEnum(BoardStatus)

 

- 400 ERROR : NOT CREATE because of new ValidationPipe({whitelistL true, forbidNonWhiteListed: true})


CRUD( => Read )

- STATUS 200 : Read all boards 

 

- STATUS 200 : Read a board

 

- 404 ERROR : Read a board with unavailable id


CRUD( => Update )

- STATUS 200 : Update a board for title and description

 

STATUS 200 : Update a board for titl, description, status

 

- 400 ERROR : non-whitelisted value from the client

 

- 400 ERROR : update description and status, which is not an option in enum 


CRUD( => Delete )

- STATUS 200 : delete a board

 

- 404 ERROR : delete a board with unavailable board ID