ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Nest.js] 8 - CRUD summary code(#1 ~ #7)
    Nest.js 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

Designed by Tistory.