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);
'JavaScript&TypeScript' 카테고리의 다른 글
JavaScript 배열 자르기 (0) | 2023.03.29 |
---|---|
JavaScript 배열 중간에 요소 추가 및 삭제 (0) | 2023.03.29 |
JavaScript로 GCS에 파일 업로드 및 다운로드 (0) | 2023.03.17 |
Puppeteer 감시 방지 (0) | 2023.02.14 |
TypeScript 프레임워크 비교 (0) | 2022.09.27 |