클라이언트로부터 들어오는 요청을 처리하고 응답을 클라이언트에 반환
라우팅 매커니즘에 따라 요청에 따라 적합한 컨트롤러에 매핑된다.
각 컨트롤러에는 각기 다른 작업을 수행하는 메서드가 하나 이상있다.
기본 컨트롤러를 만들기 위해 클래스와 데코레이터(@)를 사용하며, 데코레이터는 클래스를 필수 메타데이터와 연결하고, nest가 라우팅 맵을 만들 수 있도록 함(요청을 해당 컨트롤러에 연결)
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
@Controller()
데코레이터 안 path에는 cats
라는 접두사를 지정함으로써 관련 라우트 집합을 쉽게 그룹화하고 반복코드를 최소화했다.
@Get()
데코레이터에 의해 nest는 GET /cats
요청을 해당 핸들러에 매핑한다.
이러한 과정을 거쳐 findAll() 메서드를 실행하고 상태코드 200과 문자열을 반환한다. 이유는?
nest의 응답을 조작하기 위한 두가지 옵션
Standard | 요청 핸들러가 자바스크립트 객체, 배열을 반환할 때 자동으로 JSON으로 직렬화된다. 하지만, 자바스크립트 Primitive type을 반환하면 nest는 직렬화를 하지 않고 값만 보낸다. 이로인해 응답처리가 간단해진다. 또한, 응답의 상태 코드는 201을 사용하는 POST 요청을 제외하고는 기본적으로 200이다. @HttpCode(...) 데코레이터를 추가해 상태코드를 변경할 수 있다. |
---|---|
Library-Specific | method handler signiture(ex: findAll(@Res() response) )에서 @Res() 데코레이터를 사용해 삽입할 수 있는 라이브러리별(ex: Express ) 응답객체를 사용할 수 있음. 예를 들어 Express에서는 response.status(200).send() 와 같은 코드를 사용해 응답 구성 |
※주의※
핸들러가 @Res()
, @Next()
(둘다 optional)를 동시에 사용하려는 경우에는 nest가 자동으로 Standard 방식을 비활성화시켜버린다. 이를 방지하기 위해서는 Res({passthrough: true})
passthrough
옵션을 true
로 두면된다.
핸들러에서 클라이언트 요청 객체(ex: req.body)에 엑세스 해야할 경우, nest에서 제공하는 기능을 이용하면 된다.
핸들러의 시그니처에 @Req()
데코레이터를 추가함으로써 nest에게 injection 하도록 지시하면, 요청 객체에 엑세스할 수 있다.
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';
}
}
HTTP 요청 관련 데코레이터별 의미
@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 플랫폼(Express, Fastify)에서 입력과의 호환성을 위해 nest는 위와 같은 데코레이터를 제공하며, 이를 사용하는 경우 기본 라이브러리(ex: @types/express)에 대한 타입도 가져와야 최대한으로 활용할 수 있다.
또한, @Res()
또는 @Response()
를 핸들러에 삽입시 해당 핸들러에 대해 nest를 Library-Specify 모드로 설정하고 응답을 관리해야 한다.
패턴 기반 라우트도 지원한다.
@Get('ab*cd')
findAll() {
return 'This route uses a wildcard';
}
"abcd", "ab_cd", "abecd "등의 라우트 경로가 모두 포함된다. "?", "+", "*", "()"는 라우트 경로에 사용될 수 있다.
"_", "."은 문자열 기반 경로로 문자 그대로 해석된다.
핸들러 레벨에서 @HttpCode(...)
데코레이터를 추가해 상태코드를 변경할 수 있다.
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
커스텀 응답헤더를 지정하기 위해 @Header()
데코레이터나 Library-Specific 응답객체를 사용할 수 있다.
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
응답을 특정 URL로 redirection하기 위해 @Redirect()
데코레이터 또는 Library-Specific 응답객체를 사용할 수 있다.
@Redirect()
는 url과 statusCode라는 두개의 인수를 가진다. 둘 다 optional이며, 생략된 경우 statusCode의 default는 302(Found)이다.
@Get()
@Redirect('https://nestjs.com', 301)
HTTP 상태코드, redirection URL을 동적으로 지정하고싶으면, route handler method에서 다음과 같이 객체를 반환하면 된다
{
"url": string,
"statusCode": number
}
반환값에 URL이나 statusCode를 재정의 하게 되면, @Redirect()
데코레이터에 전달된 모든 인수가 덮어씌워진다
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
if (version && version === '5') {
return { url: 'https://docs.nestjs.com/v5/' };
}
}
위 예의 경우, URL이 반환 값으로 바뀌게 된다.
라우트 경로에 있는 Query Parameter 값을 가져올 수도 있다.
@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`;
}
@Controller
데코레이터는 들어오는 요청의 HTTP 호스트가 특정값과 일치여부 확인을 위해 host
옵션을 사용할 수 있다.
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
host 값을 다음과 같이 데코레이터로 받아와 메소드에서 사용할 수도 있다.
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}
@Get()
async findAll(): Promise<any[]> {
return [];
}
@Get()
findAll(): Observable<any[]> {
return of([]);
}
위 두가지 방식은 같은 결과를 생성하며, 모든 비동기 함수는 Promise
를 반환해야 하기에 위와 같은 방식들로 구현해야 한다.
이전 POST 라우터에서는 클라이언트 매개변수가 없었다. 여기에 @Body()
데코레이터를 추가해 매개변수를 넣어보겠다.
그러기 전, DTO 스키마를 결정해야 한다. DTO는 데이터가 네트워크를 통해 전송되는 방식을 정의하는 객체이다.
TypeScript 인터페이스를 사용하거나 간단한 클래스를 사용해 DTO 스키마를 정의할 수 있다.(사실 DTO만으로는 검증이 완전히 되지 않는다. validator를 이용하여 보완을 해야한다.)
다만, 클래스는 ES6 표준의 일부이므로 컴파일된 JS에서 실제 엔터티로 유지되기에 클래스를 사용하는 것이 좋다.
반면, TypeScript는 인터페이스는 트랜스파일링 중에(TypeScript -> JavaScript) 제거되기 때문에 nest는 런타임에 이를 참조할 수 없다.(이러한 부분은 Pipe에서 활용된다. 나중에 확인)
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
여러 데코레이터를 사용해 기본 컨트롤러를 만드는 예제이다.
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`;
}
}
위처럼 컨트롤러가 정의되었다. 하지만, 아직 nest는 CatsController의 존재여부를 아직 모르므로 해당 클래스의 인스턴스를 만들지 않았다.
컨트롤러는 항상 모듈에 속하므로 @Module()
데코레이터 내에 controllers
배열을 포함한다.
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
@Module({
controllers: [CatsController],
})
export class AppModule {}
이렇게 컨트롤러를 추가해주었으니 이제 nest에서는 CatsController의 인스턴스를 생성할 수 있을 것이다.
여태가지는 Standard 방식으로 응답을 조작했다.
Library-Specific 방식으로 CatsController를 재정의 해보겠다.
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({success: true});
}
}
위와 같이 하면 될 것이다. 하지만 이 방식에는 단점이 있다.
코드가 플랫폼에 종속되고, 테스트하기가 더 어려워진다.
또한 위의 예처럼 인터셉터 및 @HttpCode()/@Header()
데코레이터와 같은 nest standard 응답처리에 의존하는 nest 기능과의 호환성이 손실된다.
이 문제를 해결하기 위해서는 아까 초반에 설명했듯 아래와 같이 passthrough
옵션을 true
설정하면 된다.
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
res.status(HttpStatus.OK);
return [];
}