[Nest.js] 8 - CRUD summary code(#1 ~ #7)
Life Cycle
- Nest Application
- Reqeust
- Middleware - Global bound
- Middleware - Module bound
- Guard - Global
- Guard - Controller
- Guard - Controller, Root
- Interceptor - Global
- Interceptor - Controller
- Interceptor - Route
- Pipe - Global
- Pipe - Controller
- Pipe - Route
- Pipe - Parameter
- Controller - Method Handler, DTOs
- Service - Business logic
- Repository - TypeORM, Entity
- Interceptor - Route
- Interceptor - Controller
- Interceptor - Global
- filter - Handling Exceptions
- 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
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