NestJS Logo

원문을 읽으며 정리합니다.

Table of Contents

  1. Controllers

Controllers

컨트롤러는 흔히 백엔드 구조에서 사용하는 용어로, 클라이언트로부터 Request, Response 처리를 담당한다.

Controllers

Image by: https://docs.nestjs.com/assets/Controllers_1.png
  • NestJS에서 기본적인 컨트롤러를 만들기 위해선 classesdecorators 문법을 사용한다.

Routing

아래 CLI 명령어를 실행하면 자동으로 파일을 생성하고 루트 모듈에 컨트롤러를 등록까지 시켜준다.

$ nest g controller cats

@Controller() 데코레이터는 컨트롤러 클래스에 요구되는데, 데코레이터를 사용할때 url path prefix를 지정해서 컨트롤러를 하나의 관련된 라우터로 그룹 지을 수 있다.

사용하는 모습이 Spring Boot의 @RequestMapping 어노테이션과 비슷하다.

아래 코드처럼 그룹 지으면 CatsController는 http://localhost:3000/cats/로 라우팅 되어 동작하게 된다.

아래 코드를 copy and paste 하자.

// src/cats/cat.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 엔드포인트의 핸들러를 등록시킨다.
  • Controller의 prefix와 합쳐져 엔드포인트가 생성된다.

    • 위 코드에서는 [GET] /cats이다.
    • @Get('persian') 이런식으로 엔드포인트를 수정해서 [GET] /cats/persian으로 사용할 수도 있다.

+) request method 데코레이터(위에선 findAll)의 핸들러 메서드명은 상관쓰지 않는다. 수정 가능

위 코드에선 200 상태 코드와 해당되는 응답을 줄 것이다.

  • 위 예시에선 단순 문자열 리턴했지만 객체 형태로 리턴하면 json 형태로 response된다.

    • 스프링 부트 프레임워크 처럼 내부적으로 처리를 해준다.

어떻게 아무런 코드 없이 가능할까? 바로 NestJS가 사용하는 두가지 옵션과 관련이 있다.

  1. Standard(recommended): Controller의 request handler가 JS object나 array를 리턴하면 자동으로 JSON 형태로 직렬화 한다. 그러나 JS primitive 타입을 리턴하면 JSON으로 직렬화 하지 않고 그냥 그 값을 리턴한다.

    실험해본 결과 text/html; charset=utf-8 타입으로 response 온다.

    추가로 status code는 POST는 201, 나머지 HTTP method는 200의 default 값을 가진다. handler 메서드에 @HttpCode() 데코레이터를 사용해 변경 가능하다.(뒤에서 자세히 다룬다.)

  2. Library-specific: Express같은 라이브러리 response object를 사용할 수도 있다.

    아래처럼 @Res() 데코레이터를 핸들러 메서드 파라미터에 붙여주고
    findAll(@Res() response)
    아래처럼 이용해 응답을 할 수도 있다.
    response.status(200).send('Hello, NestJS')

+) 추가로 두 옵션을 한 핸들러에서 동시에 사용하게 되면 Standard 옵션은 자동으로 비활성화 된다. @Res()를 사용하는데 res를 이용해 응답하는 것이 아닌 JS 값을 리턴하는 경우 더 이상 기대한대로 작동하지 않는다.(response 객체를 주입받고 오직 cookies/headers 만 set 한다면 여전히 NestJS 안에 머물게 된다.) 따라서 이를 해결하기 위해선 @Res({ passthrough: true })와 같이 passthrough 옵션을 true로 설정해줘야 한다.

굳이 편하게 자동으로 해주는데 코드를 추가하면서 사용할 일이 있을까 싶다.

Request Object

client의 request 접근이 필요할땐 @Req() 데코레이터를 사용하면 된다. 이 객체는 default로 express의 request 객체를 따른다.

// src/cats/cat.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 object의 프로퍼티를 사용해도 좋지만 대부분을 데코레이터로 대신할 수 있다. @Body(), @Query() 처럼 사용해 꺼내올 수 있다. 아래 리스트는 더 다양한 데코레이터 목록 이다.

@Request(), @Req() req
@Response(), @Res() res, Libray-specific한 응답이 필요할 때
@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

여길 참고하면 커스텀 라우트 데코레이터를 만들 수도 있다.

Resources

위에서 정의한 @Get()과 동일한 형태로 보통의 HTTP request 엔트포인트를 정의할 수 있다.

  • @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), @Head(), @All()
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';
  }
}

Route wildcards

패턴 기반인 라우트도 지원한다. 아래는 asterisk *를 패턴으로 사용했다. abcd, ab_cd, abecd 모두 매칭된다,

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

?, +, *, (, )가 라우트 패스로 사용될 수 있고 정규식으로 매칭된다. 하이픈( - )과 점( . )은 문자 그대로 해석된다.

  • Get('ab.cd')/ab.cd로 매칭

Status code

위에서 언급한대로, default response status code는 항상 200이다. 단, POST → 201 @HttpCode()를 이용해 바꾸면 된다.

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

상태코드가 동적으로 바뀌는 경우엔 @Res를 사용하여 응답하고, 에러가 발생한 경우 예외를 thorw 해야 한다.

Headers

custom response header를 줄때는 @Header() 데코레이터를 사용하거나 res.header()를 직접 호출하는 방법이 있다.

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

Redirection

특정 URL로 리다이렉트 응답을 하기 위해선 @Redirect() 데코레이터를 사용하거나 res.redirect()를 직접 호출하는 방법이 있다. 아래와 같이 @Redirect()url을 넣어야 하고 선택적으로 statusCode도 설정할 수 있다.

  • statusCode는 디폴트 302( Found )이다.
@Get()
@Redirect('https://nestjs.com', 301)

만약 statusCode와 URL을 동적으로 주고 싶다면 route handler에서
{ url: string; statusCode: number } 타입의 객체를 리턴해주면 된다.

리턴된 값이 @Redirect() 데코레이터를 오버라이드 한다.

@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

동적인 값을 요청 받기 위해 사용한다. @Get() 데코레이터에 :(콜론)을 붙이고 파라미터 명을 지정할 수 있다. 그리고 그 값은 핸들러 파라미터에서 @Param() 데코레이터를 통해 사용할 수 있다.

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

추가로 프로퍼티로 접근하는 것이 아닌 바로 가져올 수 도 있다. 아래 코드를 확인하자.

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

Sub-Domain Routing

@Controller 데코레이터는 요청에 전달되는 HTTP host가 명시된 값과 일치함을 요구하기 위한 host 옵션을 가질 수 있다.

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

Fastify는 중첩 라우팅 지원이 부족하여 sub-domain routing을 할때는 디폴트로 Express 어댑터가 사용되어야 한다고 한다.

route의 동적 path 파라미터와 비슷하게 host 옵션도 subdomain 이름을 동적으로 할당시킬 수 있다.

근데 서브 도메인을 사용하려면 도메인 설정을 세팅해야 하지 않나? 그냥 이런식으로 사용가능하다고?? 언제든지 아시는분 계시면 댓글 남겨주시면 감사하겠습니다!

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

Scopes

원문을 몇번이고 읽는데도 정확히 이해가 안된다. 그래도 꿋꿋이 번역해보자면...

다른 언어를 사용했던 사람들이라면 NestJS가 들어오는 요청끼리 모든걸 공유한다는것을 기대하지 않을지도 모른다. NestJS는 데이터베이스 연결 pool, 글로벌 state의 싱글톤 서비스 등. Node.js는 각 모든 요청마다 분리된 쓰레드를 생산되는 비저장 모델 멀티 쓰레드 request/response를 따르지 않는다. 따라서 싱글톤 인스턴스를 사용함으로써 충분히 애플리케이션을 안전하게 할 수 있다.

그러나 GraphQL 애플리케이션에서 요청당 캐싱, 요청 추적 또는 multi-tenancy와 같이 컨트롤러의 요청 기반 수명이 바라는 행동이 될지 모르는 엣지 케이스가 있다. 여기서 스코프를 컨트롤하는 방법을 배울수 있다.

Asynchronicity

모든 async function은 Promise를 리턴한다. 이 뜻은 우리가 NestJS가 스스로 resolve 할 수 있는 연기된 값을 리턴할 수 있다는 것이다.

// src/cats.controller.ts
@Get()
async findAll(): Promise<any[]> {
  return [];
}

위 코드는 매우 완벽하지만 NestJS route handler에서 RxJS의 Observable streams를 리턴한다며 더욱 강력한 코드가 된다. NestJS는 자동으로 소스를 subscribe(구독)하고 stream이 끝날때 마지막으로 나온 값을 가져간다.

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

나는 RxJS와는 문외한 이므로 그려러니 하고 넘어가겠다.

Request payloads

예제의 POST route handler가 데이터를 받으려면 @Body() 데코레이션을 추가하면 된다.

그러나 먼저 DTO(Data Transfer Object)를 정의해야 한다. DTO는 네트워크 상에서 왔다갔다 하는 객체이다. DTO 스키마를 TypeScript interfaces, classes를 사용해 정의할 수 있다. NestJS에서는 둘 중 classes를 사용해 정의할 것을 추천한다. 이유는 classes는 ES6 표준 문법이고 따라서 JavaScript로 컴파일된 후에도 실제 entities를 보존할 수 있게 된다. 반면 interfaces는 컴파일 후 삭제되기 때문에 런타임에 참조할 수 없다. 이는 매우 중요한데, 왜냐하면 런타임에 이러한 값을 접근해 Pipe와 같이 추가적으로 무언가 가능케 할 수 있기 때문이다.

아직 Pipe가 뭔지는 잘 모른다.

아래 클래스를 생성하자.

// src/cats/dto/create-cat.dto.ts
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

그리고 이 DTO를 Controller에서 사용해 보자.

// src/cats/cats.controller.ts
@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

Handling errors

이 부분은 뒤에서 원문을 정리할 예정이다.

Next NestJS OVERVIEW(3) - Provider
Intro NestJS OVERVIEW(0) - Intro