Back End/NestJS

[NestJS] #2 Rest API

YJ_SW 2022. 9. 15. 17:13
728x90

2.0 Movies Controller

url을 가져오고 function을 실행하는 controller먼저 구현

nest의 CLI를 활용해 controller를 생성할 수 있다.

nest g co
// nest generate controller

movies.controller.ts, movies.controller.spec.ts 가 자동으로 생성된다.

src > movies > movies.controller.ts

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

@Controller('movies')
export class MoviesController {}

src > movies > movies.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { MoviesController } from './movies.controller';

describe('MoviesController', () => {
  let controller: MoviesController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [MoviesController],
    }).compile();

    controller = module.get<MoviesController>(MoviesController);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });
});

app.module.ts

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

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

자동으로 MoviesController controller를 import 해준다.


src > movies > movies.controller.ts

import { Controller, Get } from '@nestjs/common';

**@Controller('movies')**
export class MoviesController {
    @Get()
    getAll() {
        return 'this will return all movies';
    }
}

이와 같이 하면 localhost:3000 들어갔을 때

이렇게 에러가 발생한다.

localhost:3000/movies 로 들어가야 우리가 원하는 'this will return all movies' 이 문구를 볼 수 있다.

✏️ @Controller('movies') 부분이 url의 Entry Point를 컨트롤한다

 


특정 id의 movie 요청하기

@Get(':id')
    getOne() {
        return "This will return one movie";
    }

localhost:3000/movies/1

하면 위의 문구가 리턴된다.

id를 표출하고 싶을 때

무언가 필요하다면 내가 요청해야만 한다.

id라는 parameter를 movieId라는 argument에 string 타입으로저장하고 싶어

@Get(':id')
    getOne(@Param('id') movieId: string) { // @Param에 들어있는 인자와 위의 get :id는 같아야 한다. 
        //id라는 parameter를 movieId라는 argument에 string 타입으로저장하고 싶어
        return `This will return one movie with the id : ${movieId}`;
    }

Post

@Post()
    create(){
        return 'this will create a movie';
    }

Delete

@Delete(':id')
    remove(@Param('id') movieId: string) {
        return `this will remove a movie with the id : ${movieId}`;
    }

Patch

@Patch(':id')
    update(@Param('id') movieId: string) {
        return `this will update a movie with the id : ${movieId}`;
    }

update기능을 구현하기 위해 put은 모든 리소스를 업데이트 하기 때문에 patch를 써야할 수도 있다.

patch는 리소스의 일부분만 업데이트 해준다.

2.1 More Routes

Body Decorator

Post request로 생성 시

이와 같이 요청을 할 것이다. ( Body의 JSON은 큰따옴표 써야함 )

POST API는 요청한 body의 json을 가져와서 생성해야 할 것이다.

@Post()
    create(@Body() movieData){
        console.log(movieData)
        return 'this will create a movie';
    }
@Patch(':id')
    update(@Param('id') movieId: string, @Body() updateData) {
        return {
            updateMovie: movieId,
            ... updateData
        }
    }

업데이트할 movie의 id랑 우리가 보낼 데이터의 오브젝트를 리턴할 것이다.

✏️ 필요한 parameter를 직접 요청해야 한다 @Body 나 @Param등을 이용해서

 

search - query Parameter

localhost:3000/moives/search?year=2000 검색하고 싶을 때 ( query argument 사용해서 )

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

코드에서 search부분이 get보다 밑에 있으면 NestJS는 search를 id로 판단한다. :id get보다 위에 정의되어 있어야 한다.

2.2 Movies Servie part One

Single responsibility Principle

하나의 module, class 혹은 function이 하나의 기능은 꼭 책임져야 한다

Service

Moives의 로직을 관리하는 역할

컨트롤러는 url을 매핑하고 request를 받고, query, body를 넘기는 역할을 한다.

nest g s
// nest generate service

movies.service.ts, movies.service.spec.ts 가 자동으로 생성된다.

src > movies > movies.service.ts

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

@Injectable()
export class MoivesService {}

src > movies > movies.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { MoivesService } from './moives.service';

describe('MoivesService', () => {
  let service: MoivesService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [MoivesService],
    }).compile();

    service = module.get<MoivesService>(MoivesService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});

app.module.ts

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

@Module({
  imports: [],
  controllers: [MoviesController],
  **providers: [MoivesService],**
})
export class AppModule {}

자동으로 MoivesService service를 import 해준다.


보통은 Entities에 실제로 데이터베이스의 모델을 만들어줘야한다.

src > moives > entities > movie.entity.ts

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

service의 getAll, getOne, create, delete 구현

src > movies > movies.service.ts

import { Injectable } from '@nestjs/common';
import { Movie } from './entities/movie.entity';

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

    getAll(): Movie[]{
        return this.movies;
    }

    getOne(id:string):Movie {
        return this.movies.find(movie => movie.id === parseInt(id));
    }

    deleteOne(id:string):boolean {
        this.movies.filter(movie => movie.id !== parseInt(id));
        return true;
    }

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

수동으로 import 하는건 NestJS에서 기본적으로 쓰는 방법이 아니다.

src > movies > movies.controller.ts

import { Body, Controller, Delete, Get, Param,Patch,Post, Put, Query } from '@nestjs/common';
import { create } from 'domain';
import { Movie } from 'src/moives/entities/movie.entity';
import { MoivesService } from 'src/moives/moives.service';

@Controller('movies')
export class MoviesController {

    constructor (private readonly movieService: MoivesService) {};

    @Get()
    getAll() :Movie[]{
        return this.movieService.getAll();
    }

    @Get(':id')
    getOne(@Param('id') movieId: string):Movie { // @Param에 들어있는 인자와 위의 get :id는 같아야 한다. 
        //id라는 parameter를 movieId라는 argument에 string 타입으로저장하고 싶어
        return this.movieService.getOne(movieId);
    }
   
    @Post()
    create(@Body() movieData){
        return this.movieService.create(movieData)
    }

    @Delete(':id')
    remove(@Param('id') movieId: string) {
        return this.movieService.deleteOne(movieId);
    }
    
    
}

2.3 Movies Service part Two

따로 DB를 사용하지 않으니 파일을 저장하면 데이터베이스가 날아가버린다. 메모리 위에 있는 데이터베이스이기 때문이다.

예외처리

import { Injectable, NotFoundException } from '@nestjs/common';
import { Movie } from './entities/movie.entity';

@Injectable()
export class MoivesService {
    ...
    getOne(id:string):Movie {
        **const movie = this.movies.find(movie => movie.id === parseInt(id));
        if(!movie){
            throw new NotFoundException(`Movie with id ${id} Not Found`)
        }
        return movie**
    }

    deleteOne(id:string) {
        **this.getOne(id) // id 없을 때 에러 발생함**
        this.movies = this.movies.filter(movie => movie.id !== parseInt(id));
    }

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

    }
}

2.4 DTOs and Validation part One

create하거나 update 할 때에 entity에 정의되어 있는 형태로 들어오는지 정합성을 체크하기 위해 별도 작업이 필요하다.

updateData와 movieData에 타입을 부여해줘야 한다.

service와 controller에 DTO를 만들어야한다.

DTO : Data Transfer Object 데이터 전송 객체

src > moives > dto > create-movie.dto.ts

readOnly 이고 movie를 만들기 위해 필요한 것들을 나열할 것이다.

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

사람들이 보낼 수 있고 보냈으면 하는 형태 정의

컨트롤러와 서비스안의 create 함수의 movieData 타입은 CreateMovieDto가 될 것이다.

//movies.service.ts
create(movieData**:CreateMovieDto**) {
        this.movies.push({
            id: this.movies.length +1,
            ...movieData
        })
    }

...
// movies.controller.ts
@Post()
    create(@Body() movieData:CreateMovieDto){
        return this.movieService.create(movieData)
    }

이렇게 타입을 명시해줘도 title, year, genres 중 빠뜨려도 생성이 된다.

DTO는 프로그래머로서 코드를 더 간결하게 만들 수 있도록 해준다.

NestJS가 들어오는 쿼리에 대해 유효성을 검사할 수 있게 해준다.

이를 위해 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(new ValidationPipe())**
  await app.listen(3000);
}
bootstrap();

우리가 쓰고 싶은 파이프를 NestJS 어플리케이션에 넘겨준다. ValidationPipe()를 넘겨주면 된다. 이게 유효성 검사를 해주기 때문이다.

npm i class-validator class-transformer

src > movies > dto > 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[];
}

이와 같이 설정하고

{
	"hacked":"wrong data"
}
{
  "statusCode": 400,
  "message": [
    "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"
}

잘못된 json data create하면 이와같은 결과를 보내준다.

실시간으로 코드의 유효성체크를 해준다.

⇒ ValidationPipe와 그걸로 검사하는 CreateMovieDtop를 사용하기 때문에 잘못된 요청은 못하게 막아준다.

validationPipe 옵션

app.useGlobalPipes(new ValidationPipe(
    { 
      whitelist: true,
      forbidNonWhitelisted: true,
    }
  ))
  • whitelist

true로 설정하면 아무 Decorator도 없는 어떠한 property의 object는 거릅니다.

  • 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"
}

있으면 안되는 property는 존재하면 안된다고 알려준다.

  • transform

유저들이 보낸 값을 우리가 원하는 실제타입으로 변환해준다.

GetOne API 호출 시 movieId를 받아 처리한다.

Url로 보낸 값은 뭐든지 string이다. 하지만 entity에 저장된 id의 타입은 number이다. 기존 코드에서는 받아온 id를 parseInt(id) 와 같이 number로 변환하여 사용하였다.

이런 코드는 좋은 코드가 아니다. 이를 해결하기 위한 옵션이 존재한다.

// movies.controller.ts
...
@Get(':id')
    getOne(@Param('id') movieId: **number**):Movie { // @Param에 들어있는 인자와 위의 get :id는 같아야 한다. 
        //id라는 parameter를 movieId라는 argument에 string 타입으로저장하고 싶어
        return this.movieService.getOne(movieId);
    }

// movies.service.ts
...
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
    }
//main.ts
...
app.useGlobalPipes(new ValidationPipe(
    { 
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }
  ))

NestJS는 타입을 받아서 넘겨줄 때 자동으로 타입을 변환해준다.

url로 입력한 값이므로 string인데 받는 id를 number로 지정해주어 자동으로 string → number로 변환해준다.

2.5 DTOs and Validation part Two

src > movies > dto > update-movie.dto

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

export class CreateMovieDto {
    @IsString()
    readonly title?:string;
    @IsNumber() 
    readonly year?:number;
    @IsString({ each: true})
    readonly genres?:string[];
}

읽기전용, 제목만 수정할때도 있고 장르만 수정할 때도 있으므로 필수는 아니니 ? 붙여주었다.

이와 같이 작성해줘도 되지만 createMovieDto 와 유사하고 선택입력사항이라는 것만 다르다.

먼저 @nestjs/mapped-types 설치하기

npm i @nestjs/mapped-types

mapped-types 는 타입을 변환시키고 사용할 수 있게 해주는 패키지이다.

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

export class UpdateMovieDto extends PartialType(CreateMovieDto) {}

Nest는 PartialType() 유틸리티 함수를 제공합니다. PartialType() 함수는 입력 유형의 모든 속성이 선택 사항으로 설정된 유형(클래스)을 반환합니다.

2.6 Modules and Dependency Injection

모듈의 구조 바꾸기

✏️ 앱을 만들 때 모듈로 분리해야 좋은 구조이다.

NestJS에서 앱은 여러 개의 모듈로 구성이 된다.

app.module은 AppService와 AppController만 가져야 한다.

MovieService와 MovieController를 movies.module로 옮길 것이다.

nest g mo
// nest generate module
// app.module.ts
import { Module } from '@nestjs/common';
import { MoviesController } from './movies/movies.controller';
import { MoivesService } from './moives/moives.service';
import { MoviesModule } from './movies/movies.module';

@Module({
  **imports: [MoviesModule],**
  controllers: [MoviesController],
  providers: [MoivesService],
})
export class AppModule {}

와 같이 자동으로 변환된다.


src > movies > movies.module.ts

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

@Module({
    controllers:[MoviesController],
    providers:[moviesService]
})
export class MoviesModule {}

src > app.module.ts

import { Module } from '@nestjs/common';
import { MoviesModule } from './movies/movies.module';

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

app.module은 MoviesModule을 import하고 MoviesModule은 controller와 providers 를 가진다.

그렇다면 언제 app.module을 쓰고 Controller와 Provider를 만들면 될까?

nest g co
// nest generate controller

app.controller가 생성된다.

import { Controller, Get } from '@nestjs/common';

@Controller('')
export class AppController {
    @Get()
    home() {
        return 'Welcome to my Movie API'
    }
}

Home 화면 생성

dependency Injection

providers가 여기 있는 모든 것들을 import해서 타입을 추가하는 것만으로 잘 동작하게 해준다.

src > movies > movies.modules.ts

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

@Module({
    controllers:[MoviesController],
    **providers:[moviesService]**
})
export class MoviesModule {}

NestJS가 MoviesService를 import하고 Controller에 inject(주입)할것이다.

MoviesService를 보면 Injectable이라는 decorator가 있는데

movies.module.ts에서 providers에 MoviesService라고 명시를 해줘야 Controller에서 MovieService를 사용할 수 있다 ( constructor 부분 ).

Controller에서 따로 import MovieService 해주지 않아도 동작할 수 있는게 Dependency Injection 덕분이다. ( movies.module.ts에서 providers에 MoviesService 쓰면 알아서 가져다 쓰는 것 ) ⇒ NestJS가 알아서 import 해준다.

2.7 Express on NestJS

NestJS는 Express 위에서 돌아간다. 컨트롤러에서 Request, Response 객체가 필요하면 사용할 수 있다.

Fastify 라이브러리와 호환이 된다. ( Express 보다 2배 빠른 속도 )

NestJS가 이 두 개의 프레임워크 위에서 동시에 돌아간다.

Express에서 req, res 객체를 많이 사용하지 않는게 중요하다. 동작은 하지만 프레임워크를 바꾸고 싶을 때 문제가 될 수 있다.

ex ) res.json

 

 

출처

https://nomadcoders.co/nestjs-fundamentals

https://docs.nestjs.com/

 

728x90