Node.js/Nest.js

[NestJS] NestJS 시작하기

반응형

@nestjs/cli를 전역으로 설치를 하고 프로젝트를 생성합니다.

 

nest new로 프로젝트를 생성하실 때는 원하는 경로로 이동후 실행하셔야합니다.

$ npm i -g @nestjs/cli

$ nest new [프로젝트명]

 

그럼 app.module.ts, app.controller.ts, app.service.ts 가 src/ 폴더 안에 생성되는데 하나씩 역할을 살펴보겠습니다.

 

module

module.ts는 하나의 기능을 하는 모듈을 말합니다.

module안에는 contorller와 provider가 있을 겁니다.

 

controller

컨트롤러는 기본적으로 url을 가져오고 함수를 실행합니다. express에서 라우트같은 존재라고 할 수 있습니다. 

컨트롤러에는 데코레이터가 있습니다. 

한 가지 주의할 점은 데코레이터는 꾸며주는 함수나 클래스랑 붙어있어야하기에 줄을 띄우면 안됩니다.

 

여기서 controller에 다 넣으면 되지 왜 service가 더 필요한지 궁금해 하실 수 있습니다.

NestJS는 컨트롤러를 비즈니스로직과 구분짓기 위해서입니다.

 

service

컨트롤러는 url만 갖고오기 위함입니다. 서비스는 비즈니스로직을 갖고있으며 실제로 함수가 실행되는 곳입니다.

 

app.module.ts는 루트모듈의 개념이고 여기에 우리가 하는 모든것을 import 합니다.

 

2.0

 

영화와 관련된 CRUD를 위해 movies 컨트롤러를 생성해 보겠습니다.

$ nest g co movies

그럼 자동적으로 app.module.ts의 contorllers에 MoviesContorller가 추가 될겁니다.

 

이제 movies폴더의 movies.controller.ts 에 라우트를 추가해보겠습니다.

 

@Controller('movies')
export class MoviesController {
  constructor(private readonly moviesService: MoviesService) {}

  @Get()
  getAll() {
    return 'This will return all movies'; 
  }
}

이 경우 locahost:3000/movies 경로로 들어오면 'This will return all movies'라는 문구가 반환될 겁니다.

 

다음은 하나의 영화정보만 가져올 수 있도록 parameter를 넣어보겠습니다.

 

@Controller('movies')
export class MoviesController {
  constructor(private readonly moviesService: MoviesService) {}

  @Get()
  getAll() {
    return 'This will return all movies'; 
  }
  
  @Get('/:id')
  getOne(@Param('id') movieId: string) {
    return `This will return one movie with the id: ${movieId}`
  }
  
}

@Param이라는 데코레이터를 넣어줘야하며 id가 string이라는 타입도 넣어주었습니다.

 

다음은 Post와 Delete 요청입니다.

Post 는 Body 데코레이터를 넣어서 전달해 줄 수 있습니다.

Delete의 경우 id값을 알아야 지울 수 있기 때문에 parameter를 넣어줍니다.

 

@Controller('movies')
export class MoviesController {

  @Get()
  getAll() {
    return 'This will return all movies'; 
  }
  
  @Get(':id')
  getOne(@Param('id') movieId: string) {
    return `This will return one movie with the id: ${movieId}`
  }
  
  @Post()
  createMovie(@Body() movieData) {
    return movieData;
  }
  
  @Delete(':id')
  removeMovie(@Param(id) movieId: string) {
    return `This will delete a movie with the id: ${movieId}`;
  }
  
  @Patch(':id')
  patchMovie(@Param(id) movieId: string, @Body() updateData) {
    return {
      updatedMovie: movieId,
      ...updateData
    }
  }
}

 

search라는 경로로 query parameter를 사용해 검색한다고 가정해봅니다.

그러면 순서가 중요한데, 위에서 작성했던 :id값을 먼저 읽기 때문에 search로 시작하는 것들이 id로 인식됩니다.

따라서 다음과 같은 순서로 작성해야합니다.

다음 예제는 연도로 searching할 경우 입니다.

@Controller('movies')
export class MoviesController {

  @Get()
  getAll() {
    return 'This will return all movies'; 
  }
  
  @Get('search')
  search(@Query('year') searchingYear: string) {
    return `We are searching for a movie with a title: ${searchginYear}`;
  }
  
  @Get(':id')
  getOne(@Param('id') movieId: string) {
    return `This will return one movie with the id: ${movieId}`
  }
  
  ...
  
}

 

 

다음은 service를 만들어 보겠습니다.

서비스는 로직을 관리하는 역할입니다. 한 개의 요소가 반드시 하나의 기능을 책입져야 합니다.

 

다음 명령어로 service를 만들 수 있습니다.

$ nest g s movies

movies.service.ts가 생성된 것을 확인할 수 있습니다.

 

movies폴더 안에 entities폴더를 만들고 그 안에 movie.entity.ts 를 생성하겠습니다.

여기서는 서비스로 보내고 받을 클래스(인터페이스)를 export 합니다. 

 

// movie.entity.ts

export class Movie {
  id: number;
  title: string;
  year: number;
  genres: string[];
}

 

여기서는 가짜 DB를 사용합니다!

movies라는 배열을 정의하고 사용하겠습니다.

 

//movies.service.ts

export class MoviesService {
  private movies: Movie[] = [];
  
  getAll(): Movie[] {
    return this.movies
  }
  
  getOne(id: string):Movie {
    return this.movies.find(movie => movie.id === parseInt(id));
    // return this.movies.find(movie => movie.id === +id); // 이렇게도 숫자형으로 바꿀 수 있습니다.
  }
}

그러면 이제 service로직을 controller에서 가져와 사용해봅시다.

 

다음과 같이 생성자 함수로 movie.service.ts를 가져옵니다.

 

 

import { MoviesService } from './movies.service';

@Controller('movies')
export class MoviesController {
  constructor(private readonly moviesService: MoviesService) {}
 
  ...
}

 

그리고 컨트롤러의 각 라우트 return 부분을 바꿔보겠습니다.

@Controller('movies')
export class MoviesController {
  constructor(private readonly moviesService: MoviesService) {}

  @Get()
  getAll(): Movie[] {
    return this.moviesService.getAll();
  }
  
  ...
  
  @Get(':id')
  getOne(@Param('id') movieId: string): Movie {
    return this.moviesService.getOne(movieId);
  }

}

 

이렇게 controller와 service를 이어줄 수 있습니다.

 

만약 사용자가 존재하지 않는 id값을 요청한다면 에러를 반환해야합니다. 다음과 같이 작성할 수 있습니다.

  getOne(id: number): Movie {
    const movie = this.movies.find((movie) => movie.id === id);
    if (!movie) {
      throw new NotFoundException(`Movie with ID: ${id} not found.`);
    }
    return movie;
  }

 

 

DTO

생성하거나 수정하는 movieData에 타입을 부여하기 위해서 서비스랑 컨트롤러에 DTO라는 걸 만들어야합니다.

movies폴더에 dto폴더를 만들고 create-movie.dto.ts 를 생성해보겠습니다.

 

// create-movie.dto.ts

export class CreateMovieDto {
  readonly title: string;
  readonly year: number;
  readonly genres: string[];
}

 

그리고 다음과 같이 컨트롤러의 movieData에 CreateMovieDto라는 타입을 부여합니다.

// movies.service.ts

...

  create(movieData: CreateMovieDto) {
    this.movies.push({
      id: this.movies.length + 1,
      ...movieData,
    });
  }
  
...

 

dto를 쓴다고 dto에 정해진 형식의 데이터만 들어가는 것은 아닙니다. 그럼 왜 dto를 쓰냐?

dto는 코드를 더 간결하게 만들 수 있도록 해줍니다. 그리고 NestJS가 들어오는 쿼리에 대해 유효성을 검사할 수 있게 해줍니다.

여기서 pipe라는 것을 더 만들어 줄겁니다.

 

Pipe

유효성 검사용 파이프를 만들어 보겠습니다. 미들웨어같은 것이라고 생각할 수 있습니다.

 

main.ts에

다음코드를 추가해줍니다.

const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe()); // 이 라인 추가
await app.listen(3000);

그리고 다음 두 모듈이 필요합니다.

$ npm i class-validator class-transformer

create-movie.dto.ts를 다음과 같이 수정해줍니다.

 

import { IsNumber, IsOptional, IsString } from 'class-validator';

export class CreateMovieDto {
  @IsString()
  readonly title: string;

  @IsNumber()
  readonly year: number;

  @IsOptional()
  @IsString({ each: true }) // 배열의 각 원소를 검사한다는 의미
  readonly genres: string[];
}

 

transform: true

app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true, // 데코레이터가 없는 property의 object는 거름
    forbidNonWhitelisted: true,
    transform: true, // 우리가 entity에서 작성한대로 형변환 해줌
  }),
);

ValidationPipe에 위와같이 조건을 추가해 줄 수 있습니다. 

순서대로 

whitelist: entity 데코레이터에 없는 프로퍼티는 들어오지 않습니다.

forbidNonWhitelisted: entity 데코레이터에 없는 값이 들어오면 에러 반환합니다.

transform: 우리가 entity에 작성한 형으로 type을 변환해줍니다.

 

 

update-movie.dto.ts

 

update시 들어오는 데이터 타입을 다시 작성해줄 수도 있지만 다음과같이 create-movie.dto.ts를 상속받을 수 있습니다.

create dto와 다른점은 모든 데이터들이 optional이라는 점입니다.

export class UpdateMovieDto extends PartialType(CreateMovieDto) {}

여기서 PartialType에 에러가 생기는데 다음 모듈을 설치하고 import시켜줍니다.

 

$ npm i @nestjs/mapped-types

 

 

 

 

 

 

 

반응형

'Node.js > Nest.js' 카테고리의 다른 글

[NestJS] NestJS 시작하기  (0) 2022.04.26