NestJS 공식문서 번역 겸 공부하는 글 입니다.
의역 및 오역이 있을 수 있습니다.
https://docs.nestjs.com/controllers

Controllers

Controllers

Controller는 들어오는 request를 핸들링하고 클라이언트에게 response를 반환하는 역할.

Controllers

routing mechanism은 어떤 controller가 어떤 request를 받을지 컨트롤한다. 각각의 controller는 보통 하나 이상의 route를 갖고, 서로 다른 route는 각각 다른 일을 수행힌다.

Routing

아래 예제에서는 basic controller를 정의하는데 필요한 @Controller() 데코레이터를 사용한다. @Controller() 데코레이터는 관련된 route들을 쉽게 그룹화 할 수 있게 해주며, 코드의 반복을 줄여준다. 예를 들어, customer entity와 상호작용하는 route의 그룹이 있다면, /customers 라는 route로 묶을 수 있다. 이 경우, @Controller(‘customers’) 데코레이터를 사용하여 쉽게 그룹화 가능하다.

// cats.controller.ts

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

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

@Get() HTTP request method 데코레이터는 Nest에게 HTTP request의 특정 엔드포인트에 대한 핸들러를 만들도록 한다. 엔드포인트는 HTTP request method(위 예제에서는 GET), route path에 일치하는 경로로 라우팅 된다. route path란, controller에 선언된 prefix와 request decorator에 의해 특정된 path의 결합이다.
예제에서 모든 route에 대한 prefix(cats)를 선언하였고, decorator에는 path 정보를 추가하지 않았으므로, Nest는 GET /cats에 대한 request를 해당 핸들러에 매핑할 것이다.
만약, path prefix가 customers 이고, decorator가 @Get(‘profile’)인 경우는 GET /customers/profile 경로로 매핑될 것이다.

위의 예제에서, 해당 엔드포인트에서 GET request가 만들어지면, Nest는 request를 findAll() method에 라우팅 할 것이다. method명은 임의로 지어진 것이다. 우리는 route에 bind될 method를 선언해야하지만, Nest는 method의 이름은 신경쓰지 않는다.

이 method는 200이라는 status code와 string을 response로 반환할 것이다. 이것에 대해 설명하려면, response를 다루기 위해 Nest에서 제공하는 2가지 옵션에 대해 알아야 한다.

Standard (recommended) request handler가 Javascript object 또는 array를 반환할 때 자동으로 JSON 형태로 직렬화 된다. 그러나, Javascript 원시타입(string, number, boolean 등)을 반환할 때에는 직렬화 없이 값을 반환한다. 이것은 response handling을 간단하게 만들어 준다. 또한, response의 status code는 항상 기본값이 200이며, POST의 경우는 201을 사용한다. 이것은 @HttpCode() 데코레이터를 추가하여 간단하게 변경 가능하다.
Library-specific @Res() 데코레이터를 사용하여 library-specific response object(예를 들어, Express)를 사용할 수도 있다(예를 들어, findAll(@Res() response)). 이러한 접근법으로 native response handling method들을 사용 가능하다. 예를 들어, Express를 사용한다면 아래의 코드처럼 사용 가능하다.response.status(200).send()

Warning ibrary-specific option을 사용하기 위해 @Res()와 @Next()를 모두 사용하면 Nest는 이를 감지하여 standard는 자동으로 disabled 된다. 두 접근법을 동시에 사용하기 위해서는 passthrough option을 true로 사용해야 한다. @Res({ passthrough: true })

Request object

Handler는 가끔 request 세부사항에 접근해야 하는 경우가 있다. Nest는 platform(default는 Express)에 종속된 request object를 제공한다. @Req() 데코레이터를 사용하여 request object를 injection하여 접근 가능하다.

// cats.controller.ts

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

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}

위 예제처럼 request: Request 로 typing을 하기 위해서는 @types/express 패키지를 설치하면 된다.

request object는 query string, parameter, HTTP header, body 등의 속성값을 가진 HTTP request이다. 대부분의 경우, 속성들을 수동으로 가져올 필요 없이 @Body(), @Query()와 같은 데코레이터를 사용하면 된다. 아래는 platform-specifit object의 데코레이터 목록이다.

@Request(), @Req() req
@Response(), @Res() res
@Next() next
@Session() req.session
@Param(key?: string) req.params / req.params[key]
@Body(key?: string) req.body / req.body[key]
@Query(key?: string) req.query / req.query[key]
@Headers(name?: string) req.headers / req.headers[name]
@Ip() req.ip
@HostParam() req.hosts

HTTP platform(Express와 Fastify) 기반의 typing을 위해 Nest는 @Res(), @Response() 데코레이터를 제공한다. 이 데코레이터는 native platform의 response 오브젝트를 노출한다. 이것을 사용할 때, 기반 라이브러리를 import 해주어야 한다(예를 들어, @types/express). @Res(), @Response() 데코레이터를 사용하면, Nest에게 핸들러에 대해 Library-specific mode로 설정하게 하고, response를 관리할 수 있게 된다. 이를 통해, res.json()이나 res.send() 같은 response object의 method를 사용할 수 있다.

Resources

앞서, cats resource(GET route)를 fetch하기 위한 엔드포인트를 정의했다. 일반적으로 새로운 레코드를 만드는 엔드포인트도 제공하고 싶을것이다. 이를 위해, POST 핸들러를 사용한다.

// cats.controller.ts

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

@Controller('cats')
export class CatsController {
  @Post()
  create(): string {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

Nest는 모든 standart HTTP method에 대한 데코레이터를 제공한다: @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), @Head(). 또한 @All() 을 사용하면 모든 요청에 대한 엔드포인트를 정의한다.

Route wildcards

wildcard로 별표(*)를 사용한 패턴도 제공한다.

@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

'ab*cd' route path는 abcd, ab_cd, abecd등과 match된다. ?, +, *, () 같은 문자도 정규식의 부분으로 route path에 사용가능하다. '-', '.'은 string기반 path에 의해 문자 그대로 번역된다.

Status code

앞서 이야기했듯이, response status code는 기본값이 200이다(POST request의 경우 201). 이것은 @HttpCode() 데코레이터를 통해 핸들러 수준에서 쉽게 변경 가능하다.

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

종종 status code는 static하지 않고 여러가지 변수에 의존한다. 이런 경우, @Res() 데코레이터를 통해 library-specific response를 사용할 수 있다.

Headers

custom response header를 특정하기 위해, @Header() 데코레이터 또는 library-specific response object에서 res.header()를 호출할 수 있다.

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

Redirection

response를 특정 URL로 redirection하기 위해, @Redirect() 데코레이터 또는 library-specific response object를 통해 res.redirect()를 호출할 수 있다. @Redirect()는 url과 statusCode를 인자로 받으며 둘 모두 optional이다. statusCode의 기본값은 302 이다.

@Get()
@Redirect('https://nestjs.com', 301)

가끔, status code나 redirect URL을 동적으로 결정해야 할 때가 있다. route handler method로부터 object를 아래와 같은 형태로 반환하면 된다.

{
  "url": string,
  "statusCode": number
}

반환된 값은 @Redirect() 데코레이터의 인자를 override 할 것이다.

@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
  if (version && version === '5') {
    return { url: 'https://docs.nestjs.com/v5/' };
  }
}

Route parameters

정적 경로를 가진 route는 request의 일부로 dynamic data에 접근해야 할 경우에는 사용할 수 없다. parameter를 갖는 route를 정의하기 위해 route parameter token을 path에 추가하면 된다. 아래는 @Get() 데코레이터에 사용한 route parameter token에 대한 예제이다. 이렇게 선언된 route parameter는 @Param() 데코레이터를 통해 접근할 수 있다.

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

위 예제에서 @Param()을 통해 id에 접근하려면 params.id로 접근할 수 있다. 또는 특정 parameter token을 데코레이터에서 접근 가능하다.

@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}

Sub-Domain Routing

@Controller() 데코레이터는 host option을 받을 수 있다. HTTP host는 들어오는 request와 특정 값을 matching하기 위해 사용된다.

@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}

Fastify는 nested router에 대한 지원을 하지 않으므로, sub-domain routing을 사용할 떄에는, Express adpater를 사용해야 한다.(기본값으로 설정되어 있음)

route path와 마찬가지로, host option의 host name의 token도 동적값으로 사용가능하다. 이 방식으로 선언된 host parameter는 @HostParam() 데코레이터를 통해 사용 할 수 있다.

@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}

Scopes

Nest에서는 들어오는 request를 통해 거의 모든것이 공유된다. database에 대한 connection pool, global state에서의 singleton service 등을 갖고 있다. Node.js는 request/response Multi-Threaded Stateless Model을 따르지 않고, 각각의 request에 대해 분리된 thread에서 진행된다. 따라서, singleton instance를 사용하는 것이 우리의 애플리케이션에 안전한 방식이다.

Asynchronicity

data extraction은 대부분 비동기로 일어난다. 따라서 Nest에서는 async fuction을 지원한다. 모든 비동기 함수는 Promise를 반환한다.

// cats.controller.ts

@Get()
async findAll(): Promise<any[]> {
  return [];
}

또한, Nest route handler는 RxJS observable stream을 반환할 수 있다. Nest는 자동으로 아래의 source를 subscribe하여 마지막으로 방출된 값을 가져올 수 있다.(stream이 끝날 때)

@Get()
findAll(): Observable<any[]> {
  return of([]);
}

두 방식 모두 유효하며, 원하는 방식대로 사용하면 된다.

Request payloads

@Body() 데코레이터를 사용하여 POST route handler에서도 클라이언트로부터 parameter를 받아올 수 있다. 그러나 먼저 DTO(Data Transfer Object) 스키마를 정의해야 한다. DTO는 네트워크를 통해 데이터를 어떤 형식으로 주고받을지 정의하는 오브젝트이다. DTO는 TypeScript interface 또는 class로 정의할 수 있다. 하지만 class를 추천하는데, class는 JS ES6 standard한 문법이며, 따라서 컴파일된 JS에서도 real entity로 보존된다. 반면, TypeScript interface는 transpilation 도중 제거되기 때문에 Nest는 그것을 런타임에 호출할 수 없다. 이것은 Pipe와 같은 feature가 런타임에 변수의 메타타입에 접근하는 경우 중요하다.

// create-cat.dto.ts

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}
// cat.controller.ts

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

Handling errors

따로 분리된 에러 핸들링 페이지에서 볼 수 있다.

Full resource sample

// cat.controller.ts

import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(@Query() query: ListAllEntities) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

Getting up and running

위처럼 풀소스코드가 정의되어 있어도, Nest는 CatsController가 존재하는지 알지 못하기 때문에, 해당 클래스의 인스턴스를 생성하지 못한다. Controller는 항상 module에 속해있어야 하며, @Module() 데코레이터를 통해 포함시킨다.

// app.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

module class에 @Module() 데코레이터를 통해 포함시키면, Nest는 해당 controller를 찾아 mount한다.

Library-specific approach

library-specific response object를 injection하기 위해 @Res() 또는 @Response() 데코레이터를 사용한다.

// cat.controller.ts

import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  findAll(@Res() res: Response) {
     res.status(HttpStatus.OK).json([]);
  }
}

이 방식을 통해, response object를 더 유연한 방식으로 컨트롤 할 수 있다. 일반적으로 이러한 접근 방식은 덜 clear하며, disadvantage를 가져올 수 있으므로 조심해서 다루어야 한다. 가장 주요한 disadvantage는 코드가 platform 의존적(기반 플랫폼들은 response object에 대해 서로 다른 API를 갖고 있다.)이 되며 테스트하기가 힘들어진다는 것이다. 또한, 위의 예제와 같이, Nest standard로 제공하는 response 핸들링 기능들을 사용할 수 없다(예를 들어, @HttpCode() / @Header() 데코레이터 등). 이것을 해결하기 위해 passthrough 옵션을 사용할 수 있다.

@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}