BACKEND/NestJS

NestJS로 API 만들기 (Feat. TypeScript)

hu6r1s 2023. 6. 5. 15:16

nomadcoder를 통해서 영화 API를 만들어보면서 API를 NestJS에 대해 학습할 계획.

SetUp

nest-cli를 통해 모듈과 서비스를 간단하게 생성할 수 있다.

nest g co

위 커맨드를 입력하면 만들 컨트롤러의 이름을 정하라고 나온다. 나는 movies라는 이름으로 정했다.

이렇게 되면 movies라는 폴더가 생길 것이고 안에 컨트롤러 파일이 생성되어 있을 것이다.

또한 앱 모듈 파일에 컨트롤러가 들어가져 있는 것을 볼 수 있다.

그리고 Rest API를 쉽게 테스트할 수 있게 도와주는 insomnia 프로그램을 사용한다.

Movies Controller

간단한 Movie의 CRUD를 작성해보자.

먼저 모든 영화를 가져오기 위한 getAll

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

다음은 영화 하나를 가져오기 위한 getOne

@Get("/:id")
    getOne(@Param('id') id: string) {
        return `This will return one movie with id: ${id}`
    }

Get데코레이터와 Param데코레이터에는 id로 동일해야하며 뒤의 타입은 달라도 된다.

@Get("/:id")
    getOne(@Param('id') movieId: string) {
        return `This will return one movie with id: ${movieId}`
    }

다음은 영화를 추가하기 위한 creat이다.

@Post()
    create() {
        return "This will create movie"
    }

다음은 영화를 삭제하기 위한 remove이다.

@Delete("/:id")
    remove(@Param("id") id: string) {
        return `This will delete one movie with id: ${id}`
    }

이제 수정을 해야하는데 여기서 HTTP Request 메소드 데코레이터를 Put이나 Patch를 쓸 수 있다.

Put은 모든 정보를 수정할 때 사용하며, Patch는 정보 일부분을 수정할 때 사용한다.

@Patch("/:id")
    patch(@Param("id") id: string) {
        return `This will update one movie with id: ${id}`
    }

Movies Service

서비스는 로직을 관리하는 역할이라고 보면 된다.

마찬가지로 nest-cli를 통해 서비스 파일을 쉽게 생성할 수 있다.

nest g s

커맨드를 실행하면 이름을 정하는게 나올 것이고 이름을 정해주면 컨트롤러와 마찬가지로 앱 모듈 파일에 알아서 입력된다.

데이터베이스를 따로 구현하지 않고 구현할 것이다.

movies라는 배열을 만들고 형식을 만들어줄 것이다.

private movies: Movie[] = []

movies 폴더에 entities 폴더를 만들고 movie.entity.ts 파일을 만들었다.

// movie.entity.ts
export class Movie {
    id: number;
    title: string;
    year: number;
    genres: string[];
}

이제 영화 정보를 모두 가져오는 로직을 구현할 것이다.

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

서비스에서 로직을 구현하고나면 컨트롤러에서 어떻게 서비스에 접근해야 할까?

컨트롤러에서 constructor를 이용해야 한다.

constructor(private readonly MovieService: MoviesService){}

이렇게 해주면 서비스에 구현한 로직들을 사용할 수 있다.

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

나머지 로직도 구현하면 된다.

private movies: Movie[] = []

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

    getOne(id: string): Movie {
        const movie = this.movies.find(movie => movie.id === +id)
        if (!movie) {
            throw new NotFoundException(`Not Found movie with id: ${id}`)
        }
        return movie
    }

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

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

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

DTO

DTO는 Data Transfer Object의 약자로서, 데이터를 오브젝트로 변환하는 객체이다.

프로그래머가 코드를 더 간결하게 만들 수 있도록 해주며, NestJS가 들어오는 쿼리에 대해 유효성을 검사할 수 있게 해준다.

한번 createMovie DTO를 만들어보자.

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

이전에 엔티티를 만든 것처럼 만들어 주면 된다.

둘의 차이는 entity는 DB, DTO는 API라고 대충 생각하면 될 것 같다.

이렇게 하면 컨트롤러와 서비스에 있는 movieData의 타입으로 설정하면 된다.

Validation

데이터들의 유효성 검증을 하기 위해 몇 가지 설치해야 할 것이 있다.

npm i class-validator class-transformer

설치를 하고 나면 main.ts파일에 해당 코드를 추가해준다.

app.useGlobalPipes(
    new ValidationPipe()
  )

그러고 dto파일에서 해당 데코레이터를 사용해준다.

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

export class CreateMovieRequestDTO {
    @IsString()
    readonly title: string;
    
    @IsNumber()
    readonly year: number;

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

ValidationPipe의 인자로 whitelist와 forbidNonWhitelisted를 사용하면 보안을 더 강화할 수 있다.

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

whitelist를 true로 설정하면 유효성 검사기가 유효성 검사 데코레이터를 사용하지 않는 속성의 object를 제거한다.

forbidNonWhitelisted를 true로 설정하면 화이트리스트에 없는 속성을 제거하는 대신 유효성 검사기가 예외를 발생시킨다.

쉽게 말하자면 누군가 이상한 걸 보내면 리퀘스트 자체를 막아버리는 것이다.

transform을 사용하면 지정한 타입으로 페이로드를 자동 변환할 수 있다.

 

UpdateMovieRequestDto도 CreateMovieRequestDto처럼 만들어주면 된다.

UpdateMovieRequestDto는 CreateMovieRequestDto와 속성들이 같지만 필수적인 것은 아니다.

그래서 PartialType을 사용할 수 있다.

npm i @nestjs/mapped-types

커맨드 실행 후 UpdateMovieRequestDto에 해당 코드를 넣으면 된다.

import { PartialType } from "@nestjs/mapped-types";
import { CreateMovieRequestDto } from "./createMovie.dto";

export class UpdateMovieRequestDto extends PartialType(CreateMovieRequestDto) {}

Modules

app.module 파일에 바로 MovieController와 MovieService가 들어가있다. app.module에는 AppController와 AppService가 있는 것이 좋다. 그래서 구조를 변경할 것이다.

nest g mo

커맨드를 실행하면 MovieModule이 생성되면서 app.module에 auto로 적용이 될 것이다.

밑에 컨트롤러와 서비스는 AppController와 AppService로 변경할 것이기 때문에 이전에 들어간 값은 지워주자.

movie.module에 MoviesController와 MoviesService를 넣어주고 app.module에는 AppController과 AppService를 넣어주며 iMoviesModule을 import해준다.