JavaScript&TypeScript

Nest.js 사용법

hjkang

nest cli 사용을 위해 글로벌로 설치

$ npm i -g @nestjs/cli

 
nest cli 사용하여 폴더 생성 후 프로젝트 구조에 맞게 수정하여 사용

// crud 구조 생성
$ nest g resource ${RESOURCE_NAME}
$ nest g res ${RESOURCE_NAME}

// 컨트롤러 생성
$ nest g controller ${CONTROLLER_NAME}
$ nest g co ${CONTROLLER_NAME}

// 서비스 생성
$ nest g service ${SERVICE_NAME}
$ nest g s ${SERVICE_NAME}

// 모듈 생성
$ nest g module ${MODULE_NAME}
$ nest g mo ${MODULE_NAME}

Controller

  • 클라이언트로부터 들어오는 요청을 처리하고 응답값을 반환
  • express의 router

src/account/controller.ts

import {
  Controller,
  Get,
  Param,
  Post,
  Put,
  Delete,
  Query,
  Body,
} from '@nestjs/common'
import { AccountService } from '@/account/service'
import { Account } from '@/entity/account'
import { Project } from '@/entity/project'
import { CreateAccount, UpdateAccount } from '@/account/dto'

@Controller('account')
export class AccountController {
  constructor(private readonly service: AccountService) {}

  @Get()
  async findAll(
    @Query('reseller_seq') reseller_seq: number,
    @Query('active') active = null,
  ): Promise<Account[]> {
    return await this.service.find(reseller_seq, active)
  }

  @Get('/resellers')
  async findResellers(): Promise<Account[]> {
    return await this.service.findResellers()
  }

  @Get('/:id')
  async findOne(@Param('id') id: string): Promise<Account> {
    return await this.service.findOne(id)
  }

  @Get('/:id/projects')
  async findProjects(@Param('id') id: string): Promise<Project[]> {
    return await this.service.findProjects(id)
  }

  @Post()
  async create(@Body() account: CreateAccount) {
    return await this.service.create(account)
  }

  @Put('/:id')
  async update(@Param('id') id: string, @Body() account: UpdateAccount) {
    return await this.service.update(id, account)
  }

  @Delete('/:id')
  async delete(@Param('id') id: string) {
    return await this.service.delete(id)
  }
}

Service

  • 비즈니스 로직 처리
  • 트랜잭션은 typrorm의 Connection 객체 사용

src/account/service.ts

import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository, MoreThan, Connection } from 'typeorm'
import { Account } from '@/entity/account'
import { Project } from '@/entity/project'
import { GroupAccount } from '@/entity/group-account'
import { CreateAccount, UpdateAccount } from '@/account/dto'

@Injectable()
export class AccountService {
  constructor(
    @InjectRepository(Account)
    private repository: Repository<Account>,

    @InjectRepository(Project)
    private projectRepository: Repository<Project>,

    private connection: Connection,
  ) {}

  async find(reseller_seq: number, active: boolean): Promise<Account[]> {
    const where = { reseller_seq }
    if (active !== null) {
      where['active'] = active
    }
    return await this.repository.find(where)
  }

  async findResellers(): Promise<Account[]> {
    return await this.repository.find({
      active: true,
      reseller_seq: MoreThan(0),
    })
  }

  async findOne(id: string): Promise<Account> {
    return await this.repository.findOne(id)
  }

  async findProjects(id: string): Promise<Project[]> {
    return await this.projectRepository.find({
      account_id: id,
    })
  }

  async create(account: CreateAccount): Promise<void> {
    await this.repository.insert(account)
  }

  async update(id: string, account: UpdateAccount): Promise<void> {
    await this.repository.update(id, account)
  }

  async delete(id: string): Promise<void> {
    await this.connection.transaction(async (manager) => {
      await manager.delete(Account, {
        id,
      })
      await manager.delete(GroupAccount, {
        account_id: id,
      })
    })
  }
}

Module

  • providers: 해당 모듈 내에 nest injector 에 의해 인스턴스화 해야하는 provider의 리스트
  • controllers: 해당 모듈 내에 정의돼있는, 인스턴스화 해야하는 컨트롤러의 리스트
  • imports: 해당 모듈에 import된 모듈의 리스트
  • exports: 해당 모듈을 import할 다른 모듈에서 사용되어야 하는 provider의 리스트
  • forFeature(): 현재 범위에 등록할 repository를 정의
  • (repository를 모듈에 등록해둬야 해당 Service provider에서 @InjectRepositoy를 통해 inject 가능)

src/account/module.ts

import { Module } from '@nestjs/common'
import { AccountController } from '@/account/controller'
import { AccountService } from '@/account/service'
import { Account } from '@/entity/account'
import { Project } from '@/entity/project'
import { GroupAccount } from '@/entity/group-account'
import { TypeOrmModule } from '@nestjs/typeorm'

@Module({
  imports: [TypeOrmModule.forFeature([Account, Project, GroupAccount])],
  controllers: [AccountController],
  providers: [AccountService],
})
export class AccountModule {}

Entity

  • 참고: TypeORM 데코레이터 씹어먹기
  • DB 테이블과 매핑(기본값은 테이블명 = 클래스명이지만 명시적으로 선언 가능)
  • class-validator로 유효성 검사
  • @Column() - entity의 속성을 테이블 컬럼으로 표시
  • @PrimaryColumn() - PK
  • @PrimaryGeneratedColumn() - 자동 생성되는 ID 값으로 increment(기본값, auto_increment), uuid 옵션 사용 가능
  • @CreateDateColumn() - 데이터 추가된 시간 자동 기록(옵션 적지 않을 시 기본값 datetime 타입)
  • @UpdateDateColumn() - 데이터 수정된 시간 자동 기록(옵션 적지 않을 시 기본값 datetime 타입)
  • @DeleteDateColumn() - 데이터 열을 실제로 삭제하지 않고 deletedAt을 사용하는 방식(update)
  • - service에서 .softDelete() 하여 사용
  • @OneToOne() - 테이블 간 1:1 관계
  • @ManyToOne()/@OneToMany() - 테이블 간 1:M 관계
  • @ManyToMany() - 테이블 간 M:N 관계
  • @JoinColumn() - @OneToOne()에서는 꼭 적어줘야하며, 테이블에 자동으로 컬럼명과 참조 컬럼명을 합친 이름의 컬럼 생성
  • @JoinTable() - M:N 관계에서 사용하며 연결 테이블 설정

src/entities/account.ts

import {
  IsDefined,
  IsObject,
  IsString,
  IsNumber,
  IsBoolean,
  IsOptional,
} from 'class-validator'
import { Exclude } from 'class-transformer'
import {
  Entity,
  PrimaryColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
} from 'typeorm'

@Entity()
export class Account {
  @PrimaryColumn()
  @IsString()
  @IsDefined()
  id: string

  @Column()
  @IsString()
  @IsDefined()
  name: string

  @Column()
  @IsString()
  @IsDefined()
  description: string

  @Column()
  @IsString()
  @IsOptional()
  master_id: string

  @Column()
  @IsString()
  @IsDefined()
  type: string

  @Column()
  @IsBoolean()
  @IsOptional()
  active: boolean

  @Column('simple-json')
  @IsObject()
  @IsDefined()
  secret_key: { secret_id: string; secret_key: string }

  @Column()
  @IsNumber()
  @IsDefined()
  reseller_seq: number

  @CreateDateColumn()
  @IsOptional()
  created_at: Date

  @UpdateDateColumn()
  @IsOptional()
  updated_at: Date

  @Exclude()
  company_seq: number

  @Exclude()
  resource_seq: number
}

DTO

  • 각 계층간의 데이터 교환을 위한 객체
  • entity를 만들고 dto를 만드는 건 중복된 작업이 많으므로 Mapped Type을 사용하여 중복 방지
  • -> dto에서 하던 유효성 검증을 entity에서 하게 수정

Mapped Type?
PartialType(): 모든 필드를 optional하게 만들어 줌
- 보통 create와 update에 들어가는 필드는 똑같은 타입이지만, create는 모두 required, update는 모두 optional인 경우가 많으므로 update에서 주로 사용

export class UpdateUser extends PartialType(CreateUser) {}


PickType(): 특정 필드만 골라서 사용

export class UpdateUser extends PickType(CreateUser, ['email'] as const) {}


OmitType(): 특정 필드만 제거해서 사용

export class UpdateUser extends OmitType(CreateUser, ['email'] as const) {}


Intersection(): 두개의 Entity를 하나의 타입으로 만들어서 사용

export class UpdateUser extends IntersectionType(CreateUser, AdditionalUser) {}

 
src/account/dto/create.ts

import { Account } from '@/entities/account'
import { OmitType } from '@nestjs/mapped-types'

export class CreateAccount extends OmitType(Account, []) {}

src/account/dto/update.ts

import { Account } from '@/entities/account'
import { PartialType } from '@nestjs/mapped-types'

export class UpdateAccount extends PartialType(Account) {}

 
원래 위와 같이 작성하였으나 파일 하나로 수정
 

src/account/dto.ts

import { Account } from '@/entity/account'
import { OmitType, PartialType } from '@nestjs/mapped-types'

export class CreateAccount extends OmitType(Account, []) {}
export class UpdateAccount extends PartialType(Account) {}

테이블 조인

# entity에서 OneToOne, ManyToOne, ManyToMany 설정

@OneToOne(() => Reseller)
@JoinColumn({ name: 'reseller_seq' })
reseller: Reseller;

 
# find options

  • 보기에 훨씬 간단하나 조인된 테이블 컬럼 명시적으로 선택 불가
  • 객체로 반환
// relations 사용(자동 left join)
await this.repository.find({
  relations: ['reseller'],
  where: {
    deleted_at: IsNull(),
  },
});

// join 사용
await this.repository.find({
  join: {
    alias: 'as',
    innerJoinAndSelect: {
      reseller: 'as.reseller',
    },
  },
});

 
# query builder

  • getMany()를 사용하면 객체로 반환
  • getRawMany() 또는 execute()를 사용시 raw data로 반환되지만 컬럼명 앞에 alias가 prefix로 붙음
  • (ex. as_seq, r_id, r_name…)
  • 조인된 테이블 컬럼 명시적으로 선택 가능
await this.repository
  .createQueryBuilder('as')
  .select(['as', 'r.id', 'r.name'])
  .leftJoin('as.reseller', 'r')
  .where('deleted_at IS NULL')
  .getMany(); // getRawMany(), execute()

미들웨어

  • 미들웨어는 라우터 이전에 호출되는 함수로, next() 사용

# 종속성 필요한 경우

logger.middleware.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}

app.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats'); // 컨트롤러도 가능 .forRoutes('CatsController');
  }
}


# 종속성 필요하지 않은 경우(Functional middleware)

logger.middleware.ts

import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
};

app.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(logger)
      .forRoutes(CatsController);
  }
}

 
# Multiple middleware
- apply() 내에서 쉼표로 구분

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

 
# Global middleware
- main.ts에서 use() 사용하거나 AppModule에서 .forRoutes('*') 사용

main.ts

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);