본문 바로가기

Backend/NestJS

[NestJS] User API 만들기(1)(Controller, 컨트롤러)

728x90
NestJS의 컨트롤러에 대한 공부 기록이다.

앞선 overview 기록에서 컨트롤러를 간단하게 다뤄봤다.

 

[NestJS Overview] controllers 파일(참조 순서, Routing)

NestJS의 controllers 파일에 대한 공부 기록이다. Controllers 기능 Controllers는 client의 request를 받고, response를 제공하는 역할을 한다. client의 요청 url router를 분리하는 기능도 한다. 위의 사진에서 볼 수

choi-records.tistory.com

컨트롤러의 역할과 전체적인 구조를 익혔으니 자세하게 공부해 보자.

개요

컨트롤러는 MVC 패턴에서 말하는 그 컨트롤러를 말한다.

들어오는 요청을 받고 처리된 결과를 응답으로 돌려주는 인터페이스 역할을 한다.

MVC(Model-View-Controller)

Model : 데이터와 비즈니스 로직을 관리한다.
View : 레이아웃과 화면을 처리한다.
Controller : 명령을 모델과 뷰 부분으로 라우팅 한다.

MVC는 관심사 분리를 목적으로 한 패턴이다.

컨트롤러는 엔드포인트 라우팅 메커니즘을 통해 요청을 분리한다.

 

본격적으로 들어가기에 앞서 User에 대한 API를 만들기 위한 보일러플레이트를 nest cli를 이용해 생성해 보자.

$nest generate resource [name]
$nest g resource [name]

위의 명령어를 입력하면, 사진과 같이 user 리소스에 대해 CRUD 기능을 구현할 수 있게 해 줄 보일러플레이트가 생성된다.

라우팅

컨트롤러에서는 Controller 데커레이터에서의 해당 컨트롤러가 맡은 리소스 분리
각 CRUD 메서드 데커레이터에서의 리소스에 대한 CRUD 기능으로의 라우터 분리를 해준다.

 

App Controller의 코드를 보자.

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}
  • 앞서 언급한 대로 Controller 데커레이터를 이용해 해당 컨트롤러가 맡은 리소스를 명시하는데, App Controller는 루트 경로에 대한 컨트롤러이므로 리소스 이름이 명시되어있지 않다.
  • Get 데커레이터는 Controller 데커레이터에서 분리된 리소스를 처리하는 라우터이다. 루트 경로에 대한 Get 요청을 처리하는 것을 볼 수 있다.

Postman을 이용해 보면, getHello 메서드의 반환값인 문자열을 볼 수 있다.

와일드카드
라우팅 패스는 와일드카드를 이용해서 작성할 수 있다.
예를 들어 @Get('he*lo')와 같이 작성하면, *자리에 어떤 문자열이 와도 상관없다는 의미이다.

End Point

End Point는 HTTP Request Method(GET, POST 등)와 Route path에 해당한다.

Route path는 의미 그대로 중복을 가지긴 하지만 Route의 경로이다.

 

Controller는 선택적으로 미리 경로를 선언할 수 있다.

@Controller('users')
export class UsersController {}

위의 코드를 보면 users라는 경로를 미리 선언했다.

즉 users/ 이후의 경로를 지정한다는 말이다.

 

이후에 메서드 데커레이터에 선택적으로 추가적인 경로를 지정한다.

  @Post('/login')
  async login(@Body() dto: UserLoginDTO) {
    const { email, password } = dto;
    await this.usersService.login(email, password);
  }

Nest는 /users/login의 route path와 Post 메서드를 맵핑해서 login이라는 route handler에 전달하는 것이다.

요청 객체

Nest는 요청과 함께 전달되는 데이터
데커레이터를 이용해 쉽게 원하는 데이터를 얻을 수 있도록 해준다.

Req 데커레이터를 사용해 보자.

import { Controller, Get } from '@nestjs/common';
import { Req } from '@nestjs/common/decorators';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(@Req() req: Request): string {
    console.log(req);
    return this.appService.getHello();
  }
}

Req 데코레이터를 이용해 요청 정보를 콘솔에 띄워봤다.

요청을 보내면 요청 객체가 어떻게 구성되어 있는지 볼 수 있다.

 

응답

앞서 만든 user의 컨트롤러에서 user 리소스에 대한 CRUD 요청을 컨트롤러에서 라우팅 하면서
응답에 관련된 기능을 보자.
  • Users 리소스에 대한 CRUD 요청
경로 HTTP 메서드 응답 상태 코드 본문
/users POST 201 This action adds a new user
/users GET 200 This action returns all users
/users/:id GET 200 This action returns #id user
/users/:id PATCH 200 This action updates #id user
/users/:id DELTE 200 This action removes #id user

응답 코드 변경

Nest에서는 CRUD에 대한 성공 응답을 @HttpCode 데커레이터를 이용해 쉽게 바꿀 수 있게 해 준다.

@HttpCode(202)
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.usersService.update(+id, updateUserDto);
}

위와 같이 HttpCode 데커레이터로 응답 코드를 변경해 주면,

응답 코드가 변경된 것을 볼 수 있다.

 

예외처리

요청에 대한 응답으로 에러나 예외를 어떻게 발생시킬까

user의 id를 1 이상으로 제한해 보자.

  @Get(':id')
  findOne(@Param('id') id: string) {
    if (+id < 1) {
      throw new BadRequestException('id는 0보다 커야 한다.');
    }
    return this.usersService.findOne(+id);
  }

nest에서 제공하는 예외 클래스를 이용해 예외를 응답으로 반환한다.

정상적으로 예외가 반환된 것을 볼 수 있다.

헤더

nest에서는 Header 데커레이터로 응답 헤더 또한 자동 구성해 준다.
  @Header('Test', 'Test Value')
  @Get(':id')
  findOne(@Param('id') id: string) {
    if (+id < 1) {
      throw new BadRequestException('id는 0보다 커야 한다.');
    }
    return this.usersService.findOne(+id);
  }

Header 데커레이터의 첫 번째 인자는 헤더의 이름, 두 번째 인자는 헤더의 값이 전달된다.

정상적으로 헤더가 추가된 것을 볼 수 있다.

리다이렉션

서버가 요청을 처리한 후, 다른 url로 이동시키고 싶을 수 있다. 이를 리다이렉션이라고 한다.
nest에서는 Redirect 데커레이터를 이용해 쉽게 구현할 수 있다.
  @Redirect('https://nestjs.com', 301)
  @Get(':id')
  findOne(@Param('id') id: string) {
    if (+id < 1) {
      throw new BadRequestException('id는 0보다 커야 한다.');
    }
    return this.usersService.findOne(+id);
  }

요청을 보내니 리다이렉트 된 것을 볼 수 있다.

요청 처리 결과에 따라 동적으로 리디렉트 하고자 한다면
응답으로 다음과 같은 객체를 반환하면 된다.
{
    "url": string,
    "statusCode" : number
}

라우트 매개변수

앞서 users/:id처럼 url에 데이터를 포함해 요청을 보낼 수 있다.
데이터를 받는 방법은 두 가지가 있다.
1. 객체 형식
2. 매개변수를 따로 받는 방식

라우트가 ':userId/memo/:memoId'라고 가정해 보자.

객체 형식

객체로 한 번에 받을 수 있다.

  @Get(':userId/memo/:memoId')
  findOne(@Param() param: { [key: string]: string }) {
    return `userId : ${param.userId} memoId:${param.memoId}`;
  }

위와 같이 여러 매개변수를 받는 객체의 타입을 지정해 주고,

받은 매개변수들을 문자열로 반환해 보자.

매개변수를 따로 받는 방식

Param 데코레이터에 매개변수를 명시함으로써 따로 받을 수도 있다.

  @Get(':userId/memo/:memoId')
  findOne(@Param('userId') userId: string, @Param('memoId') memoId: string) {
    return `userId : ${userId} memoId:${memoId}`;
  }

같은 결과를 확인할 수 있다.

하위 도메인 라우팅

api 요청을 api.도메인으로 받고 싶다고 하면
하위 도메인 라우팅 기법을 이용해 구현할 수 있다.

App module에 ApiController를 추가해 준다.

//app.module.ts
import { Module } from '@nestjs/common';
import { ApiController } from './api.controller';
import { AppController } from './app.controller';
import { AppService } from './app.service';

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

AppController에서 이미 루트 라우팅 경로를 가진 엔드포인트가 존재한다.

ApiController에서도 같은 엔드포인트를 받을 수 있게 먼저 처리되도록 순서를 정한다.

//api.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller({ host: 'api.localhost' })
export class ApiController {
  @Get()
  getHello(): string {
    return 'hello sub!';
  }
}

위처럼 하위 도메인을 지정해 주면,

설정한 도메인에 대한 요청을 처리하는 것을 볼 수 있다.

페이로드 다루기

요청에는 body에 처리에 필요한 데이터를 같이 보내는 경우가 있다.
여기서 데이터를 페이로드, body라고 한다.

NestJS에는 데이터 전송 객체(Data Transfer Object, DTO)가 구현되어 있어 쉽게 다룰 수 있다.

user를 생성하는데 필요한 CreateUserDTO를 작성해 보자.

앞서 만든 user 관련 보일러플레이트에 포함되어 있다.

//create-user.dto.ts
export class CreateUserDto {
  name: string;
  email: string;
}
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    const { name, email } = createUserDto;
    return `create user name : ${name} email : ${email}`;
  }

정상적으로 작동되는 것을 볼 수 있다.

class vs interface
DTO는 class로 구현할 수도 interface로 구현할 수 도 있다.
class는 JS 표준의 일부이므로 컴파일되어도 없어지지 않지만,
interface는 컴파일 중 제거된다.

이는 interface로 DTO를 구현할 경우 런타임에 DTO를 참조할 수 없다는 의미이다.
그래서 Nest 공식 문서에서는 DTO를 class로 구현할 것을 권장하고 있다.

Module에 추가

여기까지 컨트롤러를 생성하면 컨트롤러 클래스의 인스턴스를 생성해야 한다.

이 작업을 Module에서 해준다.

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';

@Module({
  imports: [],
  controllers: [UsersController],
  providers: [],
})
export class UsersModule {}

여기서 모듈은 모듈 데커레이터를 이용해 모듈 클래스에 메타 데이터를 첨부한다.

그렇게 함으로써 Nest는 어떤 컨트롤러 인스턴스가 생성되어야 하는지 알게 된다.


Users 컨트롤러 완성

앞서 기록을 참고해 users 컨트롤러를 완성해 보자.

 

구현할 기능은 아래와 같다.

  • user 생성
  • 이메일 확인
  • 로그인
  • user 정보 가져오기
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UserLoginDto } from './dto/UserLoginDto.dto';
import { VerifyEmailDto } from './dto/verify-email.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  //user 생성
  async create(@Body() dto: CreateUserDto): Promise<void> {
    console.log(dto);
  }

  @Post('/email-verify')
  //이메일 인증
  async verifyEmail(@Query() dto: VerifyEmailDto): Promise<string> {
    console.log(dto);
    return;
  }

  @Post('/login')
  //로그인
  async login(@Body() dto: UserLoginDto): Promise<string> {
    console.log(dto);
    return;
  }

  @Get('/:id')
  //유저 정보 받아오는 작업
  async getUserInfo(@Param('id') id: string): Promise<string> {
    console.log(id);
    return;
  }
}

 

728x90