Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

Joon's Space

[NestJS] NestJS API 만들기(movies controller, router, service, DTO) (2) 본문

Web/NestJS

[NestJS] NestJS API 만들기(movies controller, router, service, DTO) (2)

Happy Joon 2021. 7. 8. 21:38

Movies 컨트롤러 생성하기 (nest g co 명령어)

nest g co 명령어로 'movies' 라는 controller 생성

nest 명령어를 통해 영화 api에 필요한 새로 필요한 컨트롤러를 genereate 해준다. 

controller의 alias는 'co' 이므로 nest g co 이런 식으로 명령어를 사용한다. controller의 이름을 입력하면 밑의 그림과 같이 파일이 새로 생겨난다.

src 폴더안에 자동적으로 movies 파일이 생성됨.

 

<app.module.ts>

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

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

다음과 같이 자동적으로 nestjs가 MoviesController를 import 해준 것을 볼 수 있다! 

 

router

<movies.controller.ts>

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

@Controller('movies') // 엔트리 포인트를 컨트롤 하는 부분
export class MoviesController {

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

 

생성된 movies controller 에서 데코레이터를 사용하여 router기능을 사용하는데, 위에서 주의할 점은 @Controller('movies') 이 부분이 url의 entry point(엔트리 포인트)를 컨트롤하기 때문에,

 

localhost:3000/movies/ 로 url를 입력해야 controller에 있는 함수의 원하는 값이 return 된다. 

 

Parameter

nestjs 에서는 무언가가 필요하면 요청해야 한다. 

 

<movies.controller.ts>

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

@Controller('movies')
export class MoviesController {

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

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

 

위와 같이 getOne() 함수에서 요청하는 방법은 @Param()을 이용하여 parameter를 요청한다. 

@Param()을 이용하면 NestJS가 url에 있는 id parameter를 string으로 저장하여 movieId라는 이름의 변수를 사용한다고 이해한다. 

 

이때 주의해야할 점은, @Get() 안의 id, @Param() 안에 있는 id 2개 모두 이름이 같아야 한다. 

 

Decorators

<movies.controller.ts>

import { Controller, Get, Param, Post, Delete, Put, Patch } from '@nestjs/common';

@Controller('movies')
export class MoviesController {

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

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

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

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

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

위의 parameter 사용법을 이용하여, Get 이외의 decorator를 사용한다. Get, Post, Delete, Patch(부분적인 업데이트), Put(전체적인 업데이트)가 있다. 

 

Body

movie 하나를 생성한다고 했을 때, 그 영화가 가지고있는 정보를 가져오고 싶다면, post request를 했을 때 @Body decorator를 사용한다. 

 

@Post()
create(@Body() movieData) {
    return movieData;
}

@Patch('/:id')
patch(@Param('id') movieId: string, @Body() updateData){
    return {
        updateMovie:movieId,
        ...updateData,
    }; // 업데이트할 movie의 id, 내가 보낸 데이터 오브젝트를 리턴함
}

@Body decorator를 이용하여, 다음과 같이 작성한다. patch를 할 때는 @Param과 그 뒤에 @Body를 사용한다.

 

movie id가 12인 곳에 json 값을 보냈더니, 업데이트된 json 값을 return 받음

※주의할 점

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

@Get("search")
search(){
    return `We are searching for a movie with a title: `
}

다음과 같이 @Get("/:id") 밑에 @Get("search") 가 온다면, localhost:3000/movies/search를 하여도, search를 id로 생각하여 search 함수의 return 값을 받지 않는다. 따라서 위 코드의 decorator 순서를 바꾸어주어야 한다. 

 

Query 

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

query parameter를 받을 때는, @Query() 를 사용하여 search를 할 때, url에서 query문을 함수 내에서 원하는 변수에 값을 저장하여 사용할 수 있게 해 준다. 

 

위와 같이 search? 뒤의 query문의 year 값을 받아 return 해주었다. 

Movies Service 생성하기 (nest g s 명령어)

nest g s 명령어로 'movies' 라는 service 생성

controller를 생성했을 때와 마찬가지로, 명령어를 통해서 movies.service.ts를 생성한다. nestjs가 자동적으로 module.ts 에 service를 import 해준다. 

 

이제 새로 생성된 movies.service.ts 에 기존 controller 에 임시로 들어갔던 함수의 비즈니스 로직을 작성해준다. 

 

임시 Database 생성

 

<entities/movie.entity.ts>

export class Movie {
    id: number;
    title: string;
    year: number;
    genres: 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 === +id); // +id 는 parseInt(id) 와 같은 역할
    }

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

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

controller 에서 사용했던 logic을 그대로 service에 넣고, service 내에 있는 함수들을 controller에서 사용한다.

 

<movies.controller.ts>

import { Controller, Get, Param, Post, Delete, Put, Patch, Body, Query } from '@nestjs/common';
import { MoviesService } from './movies.service';
import { Movie } from './entities/movie.entity';
@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 made after : ${searchingYear}`
    // }

    @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')
    patch(@Param('id') movieId: string, @Body() updateData){
        return {
            updateMovie:movieId,
            ...updateData,
        }; // 업데이트할 movie의 id, 내가 보낸 데이터 오브젝트를 리턴함
    }


}

여기서 service내의 함수와, controller내의 함수의 이름은 중복되어도 상관없다. 그리고 this를 사용하기 위해, MoviesController class 내부 맨 위에 constructor를 정의해주어야 한다.

 

개선해야 할 점

 

GET 리퀘스트를 보냈을 때, 존재하지 않는 id를 url에 입력한다면 예외 처리를 해주어야 한다. NestJS에서는 이러한 예외처리 기능을 기본적으로 제공한다. 

 

<movies.service.ts>

import { Injectable, NotFoundException } 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 {
        const movie = this.movies.find(movie => movie.id === +id); // +id 는 parseInt(id) 와 같은 역할
        if(!movie){
            throw new NotFoundException(`Movie with ID ${id} not found.`); // NestJS 가 제공하는 예외 처리
        }
        return movie;
    }

    deleteOne(id: string) {
        this.getOne(id); // getOne이 동작한다면 밑의 동작도 실행 될 것임.
        this.movies = this.movies.filter(movie => movie.id !== +id); // this.movies를 delete한것으로 update해줌.
    }

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

    update(id:string, updateData){
        const movie = this.getOne(id);
        this.deleteOne(id);
        this.movies.push({...movie, ...updateData }); // 과거의 movie data에 updateData를 push 해줌.
    }
}

위 코드와 같이, getOne을 했을 때, 존재하지 않는다면 NotFoundException() 을 사용하여 처리해준다. 

delete, update 경우에도 맨 위에 getOne 함수를 실행시켜 존재하지않으면 동일한 에러가 나오게 처리해준다. 

 

존재하지 않는 id를 입력했을 때, 의도한 error 메세지가 잘 나오는 것을 확인할 수 있다.

 

DTO

DTO는 데이터 전송 객체(Data Transfer Object)를 말하며, updateData와 movieData에 타입을 부여하기 위해 service, controller에 DTO를 생성해야 한다. 사용하는 이유는 코드를 간결하게 만들고, NestJS가 들어오는 쿼리에 대해 유효성을 검사할 수 있게 해 준다. 

 

movie 에 필요한 정보들을 class안에 적는다.

 

<dto/create-movie.dto.ts>

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

 

그리고 movie를 생성할 때 controller에서 @POST 리퀘스트를 할 때 리턴 값 타입을 CreateMovieDto로 지정한다.

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

 

service에서 create 부분에도 리턴 값 타입을 CreateMovieDto로 지정한다.

create(movieData: CreateMovieDto){ // movieData 는 json 값 
    this.movies.push({
        id: this.movies.length + 1,
        ...movieData,
    })
}

 

 

여기서 유효성 검사를 해주기 위해서, 유효성 검사 파이프를 생성해야 한다. 파이프는 일반적으로 미들웨어 같은 거라고 보면 된다. 

우리가 사용하고 싶은 파이프를 NestJS 애플리케이션에 넘겨주기 위해서, main.ts 파일에 ValidationPipe()를 작성한다. 

 

그전에 terminal에서 class-validator를 설치해준다. (npm i class-validator class-transformer)

 

 

<main.ts>

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

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

 

class의 유효성을 확인하기 위해, create-movie.dto.ts 파일도 수정해준다.

 

<create-movie.dto.ts>

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

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

 

다음과 같이 유효성을 체크하여 CreateMovieDto class와 맞지 않아 에러메세지가 뜬 것을 확인할 수 있다. 

 

ValidationPipe 에는 여러 옵션들이 있다.

- whitelist를 true로 설정하면 설정하지 않은 정보 (ex. hacked by me)는 validator에 도달하지 않음, 즉 내가 설정하지 않은 타입의 정보를 보내면 그 리퀘스트 자체를 막아버릴 수 있다. 

- forbidNonWhitelisted를 true로 설정하면  원하지 않은 정보의 요소는 존재해선 안된다는 에러가 뜬다. 

 

ValidationPipe 옵션을 통해 다음과 같이 보안을 강화할 수 있다.

- transform을 true로 설정하면, 우리가 원하는 실제 타입으로 변환해준다. url에 있는 movieId는 원래 string 타입인데, validationpipe의 transform 옵션을 통해서 number 타입으로 변환해줄 수 있다. (NestJS는 타입을 받아서 넘겨줄 때 자동으로 타입을 변경해준다. express.js 에서는 모든 것을 스스로 변환해야 했던 것) 

 

그리고 controller, service 파일의 movieId parameter 타입을 모두 number로 바꾸어 준다. 

 

<movies.controller.ts>

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

@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 made after : ${searchingYear}`
    // }

    @Get(":id")
    getOne(@Param("id") movieId: number): Movie {
        console.log(typeof(movieId));
        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 { Movie } from './entities/movie.entity';
import { CreateMovieDto } from './dto/create-movie.dto';
import { UpdateMovieDto } from './dto/update-movie.dto';

@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.`); // NestJS 가 제공하는 예외 처리
        }
        return movie;
    }

    deleteOne(id: number) {
        this.getOne(id); // getOne이 동작한다면 밑의 동작도 실행 될 것임.
        this.movies = this.movies.filter(movie => movie.id !== id); // this.movies를 delete한것으로 update해줌.
    }

    create(movieData: CreateMovieDto){ // movieData 는 json 값 
        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 }); // 과거의 movie data에 updateData를 push 해줌.
    }
}

 

movie 정보를 생성하는 것 외에, 업데이트를 할 때도 dto가 필요하다. dto 폴더에 movie-update.dto.ts 파일을 생성하는데 여기서 CreateMovieDto와는 각 키값들을 readonly가 필수가 아닌 것만 빼면 모두 같다. 그럴 때는 '?'를 사용하면 되지만 NestJS의 기능인 PartialType을 사용한다.

 

PartialType을 사용하기 위해서 npm i @nestjs/mapped-types 명령어를 사용한다.

 

PartialType 에는 베이스 타입이 필요한데, 여기에 CreateMovieDto를 써준다. 

 

<update-movie.dto.ts>

import { IsString, IsNumber } from 'class-validator';
import { PartialType } from "@nestjs/mapped-types";
import { CreateMovieDto } from './create-movie.dto';

export class UpdateMovieDto extends PartialType(CreateMovieDto) {}

 

Module

모듈 좀 더 좋은 구조로 만들기 위해서 기존의 app.module.ts를 수정해준다. app.module.ts 에는 AppController랑 AppProvider만 가지고 있어야 하기 때문이다. 

 

따라서 MovieService, MovieController를 movies.module 에 옮겨야 한다. 

 

이때 위에서 새로운 controller와 service를 생성한 것처럼 nest 명령어를 사용한다. (nest g mo)

 

module 이름은 movies로 한다. 

 

movies 폴더에 movies.module.ts 가 생성되었다. 

app.module.ts 에있던 MoviesController, MoviesService를 삭제하고, 삭제한 내용을 movies.module.ts 에 작성해준다. 

 

<app.module.ts>

import { Module } from '@nestjs/common';
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 {}

 

여기서 app.module에 app.controller를 생성하기 위해 (nest g co)로 이름이 app 인 controller를 생성한다. 

 

<app.controller.ts>

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

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

https://localhost:3000 주소에 GET 리퀘스트를 보내면 다음과같이 home 화면이 나오는 것을 확인할 수 있다.

 

Dependency Injection

movies.controller에 내가 작성한 getAll() 함수에 this.moviesService.getAll()가 있는데, 이게 작동하는 이유는 moviesService라 불리는 property를 만들고 타입을 지정해주었기 때문이다. (typescript가 아니었다면 타입만 지정함으로써 작동하지 않았을 것임)

 

우리는 MoviesService라는 class만을 import 하고 있다. (import { MoviesService } from './movies.service';)

 

MoviesModule Controller랑 Provider import 하고 있는데, 이곳에서 모든 것이 이루어진다. 그래서 타입을 추가하는 것만으로도 모듈이 잘 작동하는 것 이다. 

 

MoviesModule에 providers를 두면 NestJS가 MoviesService를 import하고 Controller에 inject(주입) 한다. (MoviesService에 Injectable이라는 decorator가 있는 이유)

 

반응형

'Web > NestJS' 카테고리의 다른 글

[NestJS] NestJS API 만들기(controller, service)  (0) 2021.07.08