Nest의 컨트롤러는 MVC 패턴에서 말하는 컨트롤러를 의미한다. 컨트롤러는 들어오는 요청(request)를 받고 처리된 결과를 응답(response)으로 돌려주는 인터페이스 역할을 한다.
컨트롤러는 엔드포인트 라우팅 매커니즘을 통해 각 컨트롤러가 받을 수 있는 요청을 분류한다.
nest g controller [이름]
nest g resource [name] // CRUD 보일러플레이트 생성
라우팅 패스는 와일드 카드를 이용하여 작성할 수 있다. (*) 문자를 사용하면 문자열 가운데 어떤 문자가 와도 상관없이 라우팅 패스를 구성할 수 있다.
@Get('he*lo)
getHello(): string{
return this.appService.getHello();
}
이 코드는 helo, hello, he__lo 같은 경로로 요청 받을 수 있다. * 이외에 ?, +, () 문자 역시 정규 표현식에서의 와일드 카드와 동일하게 동작한다. 하지만 -, . 은 문자열 취급을 받기에 요청할 수 있다.
와일드 카드는 컨트롤러의 패스를 정할 때만 사용하는 것이 아니라, 컴포넌트에서 이름을 정할 때도 사용할 수 있다.
클라이언트는 요청을 보내면 종종 서버가 원하는 정보를 함께 전송한다. Nest는 요청과 함께 전달되는 데이터를 핸들러가 다룰 수 있는 객체로 변환한다. 이 변환된 객체는 @Req 데커레이터를 이용해 다룰 수 있다.
@Get()
getHello(@Req() req: Request): string{
console.log(req);
return this.appService.getHello();
}
요청 객체는 보통 HTTP 객체를 나타낸다. 요청 객체(req)가 어떻게 구성되어 있는지 console에 출력하면 쿼리스트링, 매개변수, 헤더 등 다양한 정보를 가지고 있다.
이 외에도 @Query(), @Param(), @Body() 데커레이터가 존재한다.
각 요청의 성공 응답 코드는 POST일 경우엔 201이고, 나머지는 200인 것을 확인할 수 있다. NestJs에서는 string, number, boolean과 같이 원시 타입을 리턴을 경우 직렬화 없이 보내지만, 객체를 리턴한다면 직렬화를 통해 json으로 자동 변환해준다.
@Get()
findAll(@Res() res) {
const users = userService.findAll()
return res.status(200).send(users);
}
위 코드처럼 @Res 데커레이터를 사용하여 Express의 응답 객체를 다룰 수 있다.
PUT과 PATCH의 차이점?
PUT은 전체 리소스를 교환할 때 사용되고, PATCH는 리소스 일부를 업데이트할 때 사용한다.
만약 200, 201처럼 상태코드를 다른 값으로 바꾸길 원한다면 어떻게 해야할까 이를 위해서 Nest에서는 또 다른 데커레이터 @HttpCode를 마련해놓았다. 밑의 예시를 보자.
@HttpCode(202)
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(+id, updateUserDto);
}
만약에 요청을 처리하던 중 에러가 발생하거나 예외를 던져야 한다면 어떻게 해야할까 예를 들어 유저가 정보 조회를 요청하였는데 id는 1부터 시작해야한다. id가 1보다 작은 값일 경우 400 Bad Request 예외를 던져야한다. 밑의 예제를 보자
@Get(':id')
findOne(@Param('id') id: string){
if( +id < 1){
throw new BadRequestException("id는 0보다 큰 값이어야 합니다.");
}
return this.usersService.findOne(+id);
}
id를 0으로 요청하면
{
"statusCode" : 400,
"message" : "id는 0보다 큰 값이어야 합니다.",
"error": "Bad Request"
}
NotFoundException 객체의 생성자로 전달한 메세지와 함께 상태 코드가 400인 에러가 발생한다.
응답이 커스텀 헤더를 추가하고 싶다면 @Header 데커레이터를 사용하면 된다. 인수로 이름과 값을 받는다. 물론 라이브러리에서 제공하는 응답 객체를 사용해서 res.header()로 직접 설정도 가능하다.
@Header('Custom', 'Test Header')
@Get(':id')
findOneWithHeader(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
서버가 요청을 처리한 후, 요청을 보낸 클라이언트를 다른 페이지로 이동하고 싶은 경우가 있다. 이를 리디렉션이라 한다.
@Redirect('https://nestjs.com', 301)
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.fineOne(+id)
}
(위 코드는 findOne 메서드가 동작하지 않고, GET(':id')로 요청을 보내면 클라이언트가 https://nestjs.com으로 리디렉션이 된다.)
요청 처리 결과에 따라 동적으로 리디렉트하고자 한다면 응답으로 다음과 같은 객체를 리턴하면 된다.
{
"url": string,
"statusCode": number
}
예를 들어 쿼리 매개변수로 버전 숫자를 전달받아 해당 버전의 페이지로 이동한다.
@Get('redirect/docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version){
if (version && version == '5'){
return { url: https://docs.nestjs.com/v5/' };
}
}
Nest는 자바스크립트 객체를 리턴하면 JSON String으로 보내게 된다.
이제 브라우저에서 http://localhost:3000/redirect/docs?version=5을 입력하면 https://docs.nestjs.com/v5로 이동한다.
(version이 5가 아닌 경우, 쿼리 파라미터가 주어지지 않은 경우 https://docs.nestjs.com로 리다이렉션된다.)
라우트(라우팅) 매개변수는 패스 매개변수라고도 합니다. 라우트 매개변수를 전달받는 방법은 2가지가 있다. 먼저 매개변수가 여러 개 전달된 경우로 한번에 받는 방법이다.
@Delete(':userId/memo/:memoId')
deleteUserMemo(@Param() params: {[key: string]: string}){
return 'userId: ${params.userId}, memoId: ${params.memoId}
}
다른 방법으로는 라우팅 매개변수를 따로 받는 것이다. REST API를 구성할 때는 라우팅 매개변수의 개수가 많아지지 않게 설계하는 것이 좋다.
@Delete(':userId/memo/:memoId')
deleteUserMemo(@Param('userId') userId: string, @Param('memoId' memoId: string){
return 'userId: ${userId}, memoId: ${memoId}
}
실제로 회사가 사용하고 있는 도메인은 example.com이고, API 요청은 api.example.com으로 받기로 하였다. 즉, http://example.com, http://api.example.com로 들어온 요청을 서로 다르게 처리하고 싶다.
또한 하위 도메인에서 처리하지 못하는 요청은 원래의 도메인에서 처리하고 싶다. 이런 경우 하위 도메인 라우팅 기법을 쓸 수 없다.
먼저 컨트롤러를 생성한다.
nest g co Api
@Controller({host: 'api.example.com'})
export class ApiControll{
@Get()
index(): string{
return 'Hello, API'
}
}
위의 코드는 host가 api.example.com으로 요청이 들어올 때만 해당 메서드를 실행할 수 있다.
POST, PUT, PATCH 요청은 보통 처리에 필요한 데이터를 실어 보낸다. 이 데이터 덩어리, 즉 Payload를 Body라고 한다. NestJS에는 데이터 전송 객체(DTO)가 구현되어 있어 본문을 쉽게 다룰 수 있다.
export class CreateUserDto{
name: string;
email: string;
}
@Post()
create(@Body() createUserDto: CreateUserDto){
const { name, email } = createUserDto;
return '유저를 생성했습니다. 이름 ${name}, 이메일: ${email}';
}
이제 유저 생성 API를 요청하고, 본문에 데이터가 잘 들어가있는지 확인해보자
실행결과는 유저를 생성했습니다. 이름: YOUR_NAME, 이메일: YOUR_EMAIL@gmail.com로 출력된다.
지금까지는 백엔드의 애플리케이션의 관문이라 할 수 있는 컨트롤러를 Nest에서 어떻게 사용하고 있는지 살펴보았다. 컨트롤러는 서버로부터 들어오는 요청을 처리하고 응답을 가공하는 것이다. 서버에서 제공하는 기능을 어떻게 클라이언트와 주고받을지에 대한 인터페이스를 정의하고 데이터 구조를 기술한다.
@Controller('user')
export class UsersController {
@Post()
async createUser(@Body() dto: CreateUserDto): Promise<void> { // 1
console.log(dto);
return;
}
@Post('/email-verity)
async verifyEmail(@Query() dto: VerifyEmailDto): Promise<string> { // 2
console.log(dto);
return;
}
@Post('/login')
async getUserInfo(@Param('id' userId: string): Promise<UserInfo> { // 3
console.log(userId);
return;
}
}
1번에서 이메일 인증 시 URL에 포함되는 쿼리 매개변수를 @Query 데커레이터와 함께 선언한 DTO로 받는다.
export class VerifyEmailDto{
signupVerifyToken: string;
}
2번은 로그인을 할 때 유저가 입력한 데이터는 본문으로 전달되도록 한다.
export class UserLoginDto {
email: string;
password: string;
}
3번은 유저 정보 조회 시 유저 아이디를 패스 매개변수 id로 받는다.
export interface UserInfo {
id: string;
name: string;
email: string;
}
이 포스팅은 한용재 님의 [Nestjs로 부르는 백엔드 프로그래밍]을 참조하여 작성하였습니다.