[NestJS] #2 Rest API
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";
}
하면 위의 문구가 리턴된다.
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