Nest.js

Seoyul Kim·2020년 9월 28일
7

Nest.js

  • express 기반으로 제작되었으며, node.js에 백엔드를 구성할 수 있도록 해준다.

  • 기본적으로 typescript를 지원하며, javascript로 어플리케이션을 작성하는 것도 가능하다.

  • 다른 node.js의 프레임워크에는 없는 구조를 가지고 있다.

  • nest.js는 구조를 갖고 있고, 그 구조 덕분에 순서와 룰도 있어서 이를 따르기만 하면 큰 규모의 백엔드를 쉽게 만들 수 있다.(미리 셋팅된 여러 기능들을 제공하기 때문에 수동으로 생성하지 않아도 된다.)

  • node.js 어플리케이션을 빌드하는데 유용하며, 객체지향 프로그래밍(Object Oriented Programming)과 함수형 프로그래밍(Functional Programming), 함수 반응형 프로그래밍(Functional Reactive Programming)의 요소도 일부분 사용한다.

setting

 npm i -g @nestjs/cli

//nest 설치 후 다음 명령어를 입력하면 폴더 이름을 묻는 질문과 함께 구조를 만들어 폴더가 생성된다.
nest new

nest-cli

  • 다음의 커맨드 라인으로 필요한 것을 생성하고 모두 import 해준다.

Controllers

  • nest.js 어플리케이션은 main.ts에서 시작한다. 하나의 모듈(AppModule(root module))에서 어플리케이션을 생성한다.

  • controller는 기본적으로 url을 가져오고 함수를 실행한다.(express의 router와 같은 역할을 한다.)

main.ts

  • 이름은 변경하지 않는다.

  • Nest application 인스턴스를 만드는 core 함수인 NestFactory를 사용하는 application의 entry file이다.

  • NestFactory는 어플리케이션 인스턴스를 만들 수 있게 해주는 몇몇의 static method들을 노출시켜 준다.

  • create()메서드는 INestApplication Interface를 수행하는 application object를 반환하며, 해당 오브젝트는 다양한 methods set을 제공한다.

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

app.module.ts

src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

app.controller.ts

  • @controller() decorator는 관련된 라우트들을 쉽게 그룹화 해주고, 반복되는 코드를 최소화해준다.

  • 에를 들어, /customers라는 라우트를 통해 customer entity와 상호작용을 위해 필요한 라우트 그룹을 형성하고 싶다면, @Controller() decorator에 customers 라는 경로 prefix를 사용할 수 있고, 해당 경로로 연결하기 위한 부분을 반복할 필요가 없다.

  • 라우팅 경로는 해당 컨트롤러를 위해 선언된 prefix와, request decorator에 선언된 특정 경로와 결합되어서 결정된다.

src/app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get() //express의 get 라우터와 같은 역할
  getHello(): string {
    return this.appService.getHello();
  }
  
  @Get('/hello')
  //string 타입을 return
  sayHello(): string {
    return this.appService.getHi();
  }
  
  @Get('search')
  search(@Query('year') searchingYear: string) {
    return `We are searching for a movie with a made after ${searchingYear}`;
  }
  
  @Get('/:id')
  getOne(@Param('id') id: string) {
    return `This will return one movie id:${id}`;
  }
  
  
  @Post()
  create() {
    return 'This will create a movie';
  }

  @Delete('/:id')
  remove(@Param('id') movieID: string) {
    return `This will delete a movie id:${movieID}`;
  }

  @Patch('/:id')
  patch(@Param('id') movieID: string) {
    return `This will patch movie id${movieID}`;
  }
  • controller는 url('/hello')의 요청을 받아 sayHello라는 함수를 실행한다.(Get decorator를 붙이면 hello 라는 url을 요청 받으면 다음 함수를 실행해야하는 것을 nest가 알고 있으며 데코레이터는 꾸며주는 함수나 클래스와 붙어있어야 한다.)

  • parameter decorator를 사용하면 nest.js는 url에 있는 id parameter를 원한다는 것을 알 수 있다.

  • @Body를 사용하여 body를 가져올 수 있고 @Query를 사용하여 쿼리 스트링을 가져올 수 있다.

Service

  • nest.js는 controller와 비즈니스 로직을 분리한다. controller는 단순히 url을 가져오는 역할을 한다.

  • 실제 function을 갖는 비즈니스로직은 서비스에서 정의한다.

app.service.ts

src/app.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
  getHi():string{
  	return 'Hi nest '
  }
}

DTO(Data Transfer Object)

  • 네트워크를 통해 데이터가 어떻게 전송될지를 정의한 객체로, DTO 스키마를 TypeScript 인터페이스를 혹은 간단한 클래스를 통해 결정할 수 있다.

  • Nest에서는 이 객체를 클래스로 정의할 것을 추천한다. 클래스는 ECMA의 표준에 속하고, 컴파일된 JS에서도 실제 entities로서 다룰 수 있지만 interface는 컴파일 과정에서 삭제되기 때문에 런타임에서 Nest가 참조할 수가 없게 된다.

  • 이러한 차이는 런타임에 이러한 메타데이터(class또는 interface로 정의된 DTO와 같은)에 접근해야 하는 Pipe와 같이 추가적인 가능성들을 더해주는 기능들이 있기 때문에 중요하다.

 npm i class-validator class-transformer          
  • ValidationPipe와 그걸로 검사하는 CreateMovieDto를 사용하여 typescript로 실시간 코드의 유효성을 체크할 수 있다.
create-movie.dto.ts

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

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

  @IsNumber()
  readonly year: number;

  @IsString({ each: true })
  readonly genres: string[];
}
main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();
  • 유효하지 않는 데이터는 validator에 도달하지 않게 whitelist를 사용할 수 있고, forbidNonWhitelisted를 사용하여 request를 막을 수 있다.
{
    "statusCode": 400,
    "message": [
        "property hacked should not exist",
        "title must be a string",
        "year must be a number conforming to the specified constraints",
        "each value in genres must be a string"
    ],
    "error": "Bad Request"
}
  • transform을 사용하면 실제 데이터 타입으로 변환해준다.

  • mapped-types는 타입을 변환시키고 사용할 수 있게 한다.(DTO를 변환시키는 것을 도와준다.)

npm i @nestjs/mapped-types    
import { PartialType } from '@nestjs/mapped-types';
import { CreateMovieDto } from './create-movie.dto';

export class UpdateMovieDto extends PartialType(CreateMovieDto) {}
  • PartialType을 이용하여 UpdateMovieDto에서 CreateMovieDto의 타입을 이용할 수 있다.

modules and dependency injection

modules

  • nest.js에서 앱은 여러개의 모듈로 구성되는데, app.module.ts에서는 app controller와 app service만 갖고 있어야 한다.

  • module은 어플리케이션의 일부분으로 장고에서 앱과 같은 의미이다.(한가지의 역할을 하는 앱으로 인증을 담당하는 어플리케이션이 있다면 users의 모듈이 될 수 있고, 인스타그램을 만든다면 사진, 비디오와 같은 모듈이 필요할 수 있다.)

  • 앱을 만들 때 모듈로 분리해야 좋기 때문에 app.module.ts 에서 import해와 사용한다.

  • 여러가지 모듈을 import 할 수 있고, nest.js가 앱을 구동하면 하나의 모듈로 생성해준다.

app.module.ts

import { Module } from '@nestjs/common';

import { MoviesModule } from './movies/movies.module';

@Module({
  imports: [MoviesModule],
  controllers: [],
  providers: [],
})
export class AppModule {}
  • providers: Nest의 injector에 의해 인스턴스화되고, 인스턴스들은 이 모듈 안에서 최소한으로 공유된다.

  • controllers: 해당 모듈에서 정의된 인스턴스화 되어야 하는 컨트롤러의 모음

  • exports: 다른 모듈에서 import 할 때 사용된다.

movies.module.ts

import { Module, Global } from '@nestjs/common';
import { MoviesController } from '../movies/movies.controller';
import { MoviesService } from '../movies/movies.service';

@Global()
@Module({ controllers: [MoviesController], providers: [MoviesService] })
export class MoviesModule {}
  • 여러 Provider들 집합을 어디서든 제공해주고 싶다면 @Global()를 사용하여 모듈을 global로 만든다.

  • @Global() 데코레이터는 모듈을 글로벌 범위로 만들어준다. 글로벌 모듈은 한 번만 등록될 수 있고, 일반적으로 루트나 코어 모듈에 의해 등록된다.

  • 어디서나 쓸 수 있고, 사용하고 싶은 모듈들은 moviesModule을 임포트 할 필요 없이 의존성 주입할 수 있다.

dependency injection

  • provider를 두면 nest.js가 service를 import 하고 controller에 inject한다.
//movies.module.ts

import { Module } from '@nestjs/common';
import { MoviesController } from '../movies/movies.controller';
import { MoviesService } from '../movies/movies.service';

@Module({ controllers: [MoviesController], providers: [MoviesService] })
export class MoviesModule {}
//movies.controllers.ts

import {
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Patch,
  Body,
  Query,
} from '@nestjs/common';
import { CreateMovieDto } from './dto/create-movie.dto';
import { UpdateMovieDto } from './dto/update-movie.dto';
import { Movie } from './entities/movie.entity';
import { MoviesService } from './movies.service';

@Controller('movies')
export class MoviesController {
  constructor(private readonly moviesService: MoviesService) {}
  @Get()
  getAll(): Movie[] {
    return this.moviesService.getAll();
  }

  @Get('search')
  search(@Query('year') searchingYear: string) {
    return `We are searching for a movie with a made after ${searchingYear}`;
  }

  @Get('/:id')
  getOne(@Param('id') movieID: number): Movie {
    return this.moviesService.getOne(movieID);
  }

  @Post()
  create(@Body() movieData: CreateMovieDto) {
    return this.moviesService.create(movieData);
  }

  @Delete('/:id')
  remove(@Param('id') movieID: number) {
    return this.moviesService.deleteOne(movieID);
  }

  @Patch('/:id')
  patch(@Param('id') movieID: number, @Body() updateData: UpdateMovieDto) {
    return this.moviesService.update(movieID, updateData);
  }
}
//movies.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateMovieDto } from './dto/create-movie.dto';
import { UpdateMovieDto } from './dto/update-movie.dto';
import { Movie } from './entities/movie.entity';

@Injectable()
export class MoviesService {
  private movies: Movie[] = [];

  getAll(): Movie[] {
    return this.movies;
  }
  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;
  }

  deleteOne(id: number) {
    this.getOne(id);
    this.movies = this.movies.filter(movie => movie.id !== id);
  }

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

  update(id: number, updateData: UpdateMovieDto) {
    const movie = this.getOne(id);
    this.deleteOne(id);
    this.movies.push({ ...movie, ...updateData });
  }
}

Express on NestJS

  • nest.js는 express, 그리고 fastify와 호환이 가능하다.(fastify는 express 보다 2배 정도 빠르다.)

  • platform-express : Express는 Node진영에서 가장 유명한 웹 프레임워크로 많은 resource들이 있고, @nestjs/platform-express 패키지를 default로 제공한다.

  • platform-fastify: Fastify는 높은 퍼포먼스와 낮은 오버헤드를 가지는 library이며, 효율성과 스피드에 중점을 두고 있다.

  • platform을 선택하면, NestFactory.create() 메서드를 사용할 때, NestExpressApplication, NestFastifyApplicaition중에서 선택해서 사용하면 된다.

Test

  • nest.js는 jest를 사용하여 테스팅 할 수 있도록 미리 셋팅되어있다.

  • .spec은 테스트를 포함한 파일이며 이 파일 이름은 convention이다. movies.controller.ts 라는 파일을 테스팅하고 싶다면, movies.controller.spec.ts라는 파일이 존재해야한다.

  • nest.js에는 jest가 .spec.ts 파일들을 찾아볼 수 있도록 설정되어있다.

unit test

  • 서비스에서 분리된 유닛을 테스트한다.(함수 단위로 테스트한다.)

e2e(end-to-end) test

  • 모든 시스템을 테스팅한다.
  • 사용자 관점에서 사용자가 취할만한 액션들을 처음부터 끝까지 테스트한다.

0개의 댓글