9시 24분
NestJS - Movie REST API 만들기 본문
이제 실전으로 가서 Movie REST API를 직접 만들어보자.
Movies Controller
Nest cli를 이용해서 controller 파일을 만들어주자.
nest generate 명령어를 사용하여 movies controller을 생성해준다.
app.module.ts에 movies 컨트롤러가 자동으로 import되며, movies.controller.ts 파일이 생성된다.
spec는 테스트 파일인데, 일단 지우고 시작하자.
movies controller에 들어가서 아래와 같이 테스트해보자.
import { Controller, Get } from '@nestjs/common';
@Controller('movies')
export class MoviesController {
@Get()
getAll(){
return 'this will return all movies';
}
}
뿜! 제대로 작동하는 것을 확인할 수 있다. 하지만 여기서 주의해야될 점이 있다 !
우리가 컨트롤러의 이름으로 'movies'를 입력했기 때문에 /movies로 들어가야 해당 내용이 뜬다. ( 라우터 역할 )
이번엔 라우터를 하나 추가해보자.
import { Controller, Get } from '@nestjs/common';
@Controller('movies')
export class MoviesController {
@Get()
getAll(){
return 'this will return all movies';
}
@Get('/:id')
getOne(){
return 'this will return one movie';
}
}
이번에도 잘 작동하는 것을 볼 수 있다 (o゚v゚)ノ
이제 크롬 브라우저에서 테스트하는 것은 그만하고, Insomnia로 테스트를 해보자.
앞에서 Insomnia 대신 Postman을 사용한다고 했는데, 어차피 둘 다 처음이고, 강의에서 Insomnia를 사용하며, Insomnia가 더 간결하다는 말을 보고 Insomnia를 사용하기로 마음을 바꿨다 ^ㅅ^
구글에 사용법을 검색해봤는데 UI가 나와 사뭇 달랐다.
나는
- Dashboard에서 프로젝트를 생성해주고,
- Request collection을 만들어준 뒤,
- 그 안에서 + 버튼을 눌러 request를 생성해주었다.
브라우저에서 테스트했을 때와 동일하게 결과를 확인할 수 있다.
이번에는 'movies/' 뒤에 나오는 id 값을 알고 싶은데, 그 방법이 express와 사뭇 다르다.
NestJS에서, 우리는 무언가가 필요하면 그것을 요청해야 한다.
그때, Param 데코레이터를 사용한다. 이 때, @Param() 안에 작성한 변수 이름과 @Get() 안에 작성한 이름은 같아야 하고, 그 뒤에 나오는 변수 이름은 달라도 괜찮다.
@Get('/:id')
getOne(@Param('id') movieId: string){
return `this will return movie #${movieId}`;
}
이번에는 Post와 Delete 데코레이터를 사용해보자.
@Post()
create(){
return 'this will create a movie';
}
@Delete('/:id')
remove(@Param('id') movieId: string){
return `this will delete movie #${movieId}`;
}
URI가 앞서 했던 Get과 같지만, HTTP 메서드가 다르므로, 겹치는 것을 걱정하지 않아도 된다!
둘 다 잘 작동하는 것을 확인할 수 있다 ~
이번엔 update 기능을 만들어보자.
update은 Put이나 Patch를 사용한다. Put은 모든 리소스를 업데이트하고, Patch는 리소스의 일부분만 업데이트한다.
@Patch('/:id')
path(@Param('id') movieId: string){
return `this will update movie #${movieId}`;
}
More Routes
위에서 배운 데코레이터들 말고 다른 데코레이터들도 더 배워보자.
request를 보낼 때, body를 추가하고 싶을 때는, @Body 데코레이터를 사용한다.
@Post()
create(@Body() movieData){
console.log(movieData);
return movieData;
}
Patch에도 똑같이 해주자.
@Patch('/:id')
path(@Param('id') movieId: string, @Body() updateData){
return {
updateMovie: movieId,
...updateData
};
}
무언가가 필요하면 그것을 요청해야 한다. 꼭 기억하자 !!
이번에는 검색 기능을 만들기 위해 query argument를 받고 싶다.
그럴 때는, Query 데코레이터를 사용한다.
@Get('/search')
search(@Query('title') movieTitle: string){
return `We are searching for a movie with a title '${movieTitle}'`;
}
그리고 주의사항 !! search 라우터가 @Get('/:id') 뒤에 있으면, search를 id로 인식하기 때문에, @Get('/:id') 앞에 작성해주어야 한다.
+ Param과 Query 비교
Param: 요청 주소에 포함되어있는 변수를 담는다.
ex) http://localhost:3000/movies/12345에서 12345를 담는다.
Query: 주소 이후 '?' 뒤에 있는 변수를 담는다.
http://localhost:3000/movies/search?title=Tenet에서 Tenet을 담는다.
어떤 resource를 식별하고 싶을 때는 Param, 정렬이나 필터링을 하고 싶을 때 Query를 사용한다.
[ 전체 코드 ]
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { query } from 'express';
@Controller('movies')
export class MoviesController {
@Get()
getAll(){
return 'this will return all movies';
}
@Get('/search')
search(@Query('title') movieTitle: string){
return `We are searching for a movie with a title '${movieTitle}'`;
}
@Get('/:id')
getOne(@Param('id') movieId: string){
return `this will return movie #${movieId}`;
}
@Post()
create(@Body() movieData){
console.log(movieData);
return movieData;
}
@Delete('/:id')
remove(@Param('id') movieId: string){
return `this will delete movie #${movieId}`;
}
@Patch('/:id')
path(@Param('id') movieId: string, @Body() updateData){
return {
updateMovie: movieId,
...updateData
};
}
}
Movies Service
single-responsibility principle을 따르자.
single-responsibility principle이란, 하나의 모듈, 클래스 혹은 함수는 하나의 기능은 꼭 책임져야 하는 것이다.
그리고, 이제까지 컨트롤러에 대해 배웠으니 이번엔 서비스에 대해 배우자.
서비스는 로직을 관리한다.
이번에도 Nest cli를 이용해서 service 파일을 생성해주자. 파일 이름은 컨트롤러와 동일하게 movies로 했다.
파일이 생성되면, 모듈 파일의 providers 부분에 자동으로 저장된다.
그 다음, entities 폴더를 생성하고 그 안에 movie.entity.ts를 만들자.
movie.entity.ts는 서비스로 보내고 받을 클래스(인터페이스)를 export할 것이다.
즉, movies를 구성하는 그 자체가 들어간다.
지금 수업에서는 데이터베이스를 다루지 않아 객체를 만들 것이지만, 보통 entities에는 실제 데이터 모델을 만들어야 한다.
export class Movie{
id: number;
title: string;
director: string;
}
그 다음, 에러가 발생하지 않도록 수정하여, 컨트롤러에 작성한 코드들을 서비스로 옮긴다.
[ movies.service.ts ]
import { Injectable } from '@nestjs/common';
import { Movie } from './entities/movie.entity';
@Injectable()
export class MoviesService {
private movies:Movie[] = [];
getAll() : Movie[]{
return this.movies;
}
getOne(id:string) : Movie {
return this.movies.find(movie => movie.id === parseInt(id));
}
create(movieData){
this.movies.push({
id: this.movies.length + 1,
...movieData
});
}
deleteOne(id:string) : boolean {
this.movies.filter(movie => movie.id !== parseInt(id));
return true;
}
}
서비스에 접근하기 위해서는 또 우리가 요청을 해야된다.
컨트롤러에서 생성자 만들어서 서비스를 사용할 수 있도록 하고, 아래 코드들도 그에 맞게 수정하자.
[ movies.controller.ts ]
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { Movie } from './entities/movie.entity';
import { MoviesService } from './movies.service';
@Controller('movies')
export class MoviesController {
constructor(private readonly moviesService: MoviesService){
}
@Get()
getAll(){
return this.moviesService.getAll();
}
@Get('/:id')
getOne(@Param('id') movieId: string) : Movie {
return this.moviesService.getOne(movieId);
}
@Post()
create(@Body() movieData){
return this.moviesService.create(movieData);
}
@Delete('/:id')
remove(@Param('id') movieId: string){
return this.moviesService.deleteOne(movieId);
}
@Patch('/:id')
path(@Param('id') movieId: string, @Body() updateData){
return {
updateMovie: movieId,
...updateData
};
}
}
( 아직 patch는 구현 안 한 상태 )
Get, Delete를 할 때, '/movies/12345'처럼 없는 movie에 접근하려는 것을 막도록 코드를 보완해보자.
NotFoundException를 사용했는데, 이는 NestJS가 제공해주는 확장 기능이다.
getOne(id:string) : Movie {
const movie = this.movies.find(movie => movie.id === parseInt(id));
if(!movie) {
throw new NotFoundException(`Movie with wrong id - ${id}`);
}
return movie;
}
deleteOne(id:string) : boolean {
this.getOne(id);
this.movies = this.movies.filter(movie => movie.id !== parseInt(id));
return true;
}
앞서 하지 않은 update도 구현해주자.
실제로는 효율적이지 않은 코드지만, 가짜 데이터베이스를 사용하고 있기 때문에 일단 이렇게 하자.
update(id:string, updateData) {
const movie = this.getOne(id);
this.deleteOne(id);
this.movies.push({ ...movie, ...updateData});
}
+ parseInt(id)를 +id로 수정해주었다. 둘 다 string을 숫자로 변환해준다는 의미이다.
DTOs and Validation
이어서 코드를 개선해보자. 우리는 항상 사용자가 보내는 데이터에 대해 의심해봐야 한다.
하지만 현재 코드에서, 우리는 updateData와 create 시 삽입하는 데이터의 유효성 검사를 진행하지 않고 있다.
updataData랑 movieData에 타입을 부여하여 유효성 검사를 하자.
그러기 위해서, 컨트롤러랑 서비스에 DTO를 만들어야 한다.
DTO는 데이터 전송 객체(Data Transfer Object)를 의미한다.
DTO를 사용하면 코드를 간결하게 만들 수 있으며, NestJS가 들어오는 쿼리에 대해 유효성 검사를 할 수 있다.
movies 폴더에 dto 폴더를 만들고, 그 안에 create-movie.dto.ts를 만들자.
create-movie.dto.ts는 기본적으로 class이다.
이제 request에 대해 생각해보자. movie 데이터는 id, title, director로 이루어져있다. 이 중 id는 우리가 전달할 수 없고, title이랑 director만 전달한다. 그러므로, title과 director에 타입을 지정해주자.
export class CreateMovieDto{
readonly title: string;
readonly director: string;
}
하지만, 이것만으로는 유효성 검사가 작동하지 않는다.
유효성 검사를 하기 위해 우리는 유효성 검사용 파이프가 필요하다.
파이프는 express에서의 미들웨어 같은 것이라고 생각할 수 있다.
main.ts에 유효성 검사용 파이프라는 것을 만들자.
app.useGlobalPipes(new ValidationPipe());
그리고 아까 만든 DTO로 유효성 검사을 할 수 있게 class-validator이랑 class-transformer 모듈을 설치해주자.
npm i class-validator class-transformer
그리고 class-validator을 이용하여 create-movie.dto.ts를 아래와 같이 수정해준다.
import { IsString } from "class-validator";
export class CreateMovieDto{
@IsString()
readonly title: string;
@IsString()
readonly director: string;
}
+ @IsString({each: true})로 하면 string 배열의 각 원소가 string인지 체크할 수 있고, @IsNumber도 있다.
ValidationPipe랑 movieDTO 덕분에 유효성 검사를 진행하고, 올바르지 않은 데이터가 왔을 때 에러가 뜨도록 할 수 있다.
ValidationPipe는 여러 옵션이 있다. 그 중 whitelist와 forbidNonWhtielisted를 사용해보자.
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true
}));
- whitelist: true - validation decorator를 사용하지 않는 속성의 유효성 검사 개체를 제거
- "hack"이 제거되어 저장됨
- forbidNonwhitelisted: ture - 화이트리스트에 없는 property validator를 제거하는 대신 예외 발생
- "hack"이라는 속성이 화이트리스트에 존재하지 않으므로 HttpException을 던짐
- 옵션을 사용하기 위해 'whitelist: true'가 선행되어야 함.
우리는 앞서, movieId의 데이터 타입을 string으로 해주었다. 그 이유는, url로 보낸 값이 뭐든 일단 string이기 때문이다. 그래서 우리는 movieId를 string으로 받아와서 number로 형변환을 해주었다.
validationPipe에 'transform: true'라는 옵션을 하나 추가해주자.
transform은 유저들이 보낸 데이터의 타입을 실제 원하는 타입으로 변환해준다.
우리는 앞서 아래와 같은 movie.entity.ts를 만들었었다.
export class Movie{
id: number;
title: string;
director: string;
}
그리고 movie.entity.ts 파일을 컨트롤러와 서비스에서 import하여 사용했다.
컨트롤러에서의 movieId랑 서비스에서 id의 타입을 number로 수정하고, '+id'로 형변환을 한 부분도 수정하자.
이제 우리는 transform 옵션을 사용하므로, string으로 들어온 id가 number로 자동으로 형변환이 된다.
이번에는 update 관련 DTO를 만들어보자.
update-movie.dto.ts 파일을 만들고, create-movie.dto.ts의 내용을 복붙해오자.
import { IsString } from "class-validator";
export class CreateMovieDto{
@IsString()
readonly title?: string;
@IsString()
readonly director?: string;
}
이번에는 create와 다르게 모든 인자가 필수이진 않게 한다. 수정을 진행할 때 전체를 수정하지 않고, 일부만 수정할 수도 있기 때문이다.
그리고 컨트롤러와 서비스의 updateData 부분에 UpdateMovieDto라는 타입을 주자.
그런데, update DTO와 create DTO는 필수 여부의 미세한 차이만 있지 거의 비슷하다.
PartialType을 사용하면 모든 필드가 선택사항이 된다. 이를 이용해서 update-movie.dto.ts 수정하자.
그 전에 PartialType을 사용하기 위해선 패키지부터 설치해야한다.
npm i @nestjs/mapped-types
mapped-types는 타입을 변환시키고 사용할 수 있게 하는 패키지이다.
그리고 update-movie.dto.ts를 아래와 같이 바꿔주자.
import { PartialType } from "@nestjs/mapped-types";
import { IsString } from "class-validator";
import { CreateMovieDto } from "./create-movie.dto";
export class UpdateMovieDto extends PartialType(CreateMovieDto){}
Modules and Dependency injection
모듈 구조 더 좋게
app.module은 AppService랑 AppController만 가져야한다.
그러므로 MoviesController와 MoviesService를 moives module로 합치자.
app.module.ts에서 controllers랑 providers 지워
import { Module } from '@nestjs/common';
import { MoviesController } from './movies/movies.controller';
import { MoviesService } from './movies/movies.service';
import { MoviesModule } from './movies/movies.module';
@Module({
imports: [MoviesModule],
controllers: [],
providers: [],
})
export class AppModule {}
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 {}
앱 모듈은 MoviesModule을 import해
그럼 앱 모듈의 controllers는 언제 쓸까. 메인 페이지 API 만들 때 굳이 모든 구조를 모듈화하여 imports할 필요는 없다.
이번엔 NestJS에 있는 dependecy injection에 대해 배워보자.
dependency injection는 우리 말로 '의존성 주입'으로, 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다.
우리가 이제까지 한 movie API로 예시를 들어보자.
컨트롤러에서 this.moviesService.getAll()을 사용하고 있고, 이게 작동하는 이유는 moviesService라 불리는 property를 만들고 타입을 지정해줬기 때문이다.
또, movies Module을 보면 providers가 모든 것을 import하여 타입 추가해주기 때문에, 우리가 타입을 지정해줄 수 있는 것이다.
dependecy injection에 대한 깊은 개념은 당장 필요 없지만, 왜 providers를 사용해야하는지 알아두자.
Express on NestJS
NestJS는 express 위에서 돌아간다.
그래서 컨트롤러에서 request, response 객체가 필요하면 @Req()나 @Res() 데코레이터를 이용할 수 있다.
그러나 req res 같은 객체를 직접 사용하는 것은 좋은 방법이 아니다.
NestJS는 express, fastify 두 프레임워크 위에서 돌아간다. 기본적으로 express 위에서 돌아가지만, fastify라는 걸로도 전환이 가능하다.
fastify는 express처럼 동작하지만 2배 더 빠르다.
그러므로, req, res 객체를 많이 사용하지 않는게 중요하다.
'Javascript > Node.js' 카테고리의 다른 글
NestJS - Unit testing과 E2E testing (0) | 2021.08.27 |
---|---|
NestJS를 들어가며 (0) | 2021.08.23 |
HTML/CSS로 프론트 작업을 할 때 주의사항 (0) | 2021.08.04 |
API와 REST API (0) | 2021.07.27 |
간단한 API 만들기 (0) | 2021.07.22 |