nestjs로 사이드 프로젝트를 하던 중 특정 api를 추가했는데, 해당 api로 라우팅이 제대로 되지 않는 요상한 현상을 발견했다.
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: '주문 목록 조회', description: '사용자가 생성한 주문 목록 조회' })
@ApiOkResponse({ type: OrderDto, isArray: true })
@Get('/orders')
getOrders(@Query() pageOption: OffsetPaginationOption, @Auth() user: User): Promise<PageResponse<OrderDto>> {
const userId = user.id;
return this.shopService.getOrdersByUserId(userId, pageOption);
}
분명 /shop/orders
로 request를 보냈는데, 엉뚱한 controller가 응답을 받고 있었다.
@ApiOperation({ summary: '매장 단건 조회', description: '매장 단건 조회' })
@ApiOkResponse({ type: ShopDto })
@Get('/:shopId')
getShopByShopId(@Param('shopId') shopId: number): Promise<ShopDto> {
return this.shopService.getShopByShopId(shopId);
}
shopId
인자의 타입이 number 였기 때문에, 다음의 에러를 보게 되었다.
[Nest] 1 - 07/22/2024, 2:18:49 PM ERROR [ExceptionsHandler] invalid input syntax for type integer: "NaN" QueryFailedError: invalid input syntax for type integer: "NaN"
at PostgresQueryRunner.query (/node_modules/typeorm/driver/postgres/PostgresQueryRunner.js:219:19)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async SelectQueryBuilder.loadRawResults (/node_modules/typeorm/query-builder/SelectQueryBuilder.js:2192:25)
at async SelectQueryBuilder.executeEntitiesAndRawResults (/node_modules/typeorm/query-builder/SelectQueryBuilder.js:2040:26)
at async SelectQueryBuilder.getRawAndEntities (/node_modules/typeorm/query-builder/SelectQueryBuilder.js:684:29)
at async SelectQueryBuilder.getOne (/node_modules/typeorm/query-builder/SelectQueryBuilder.js:711:25)
at async ShopService.getShopByShopId (/dist/shop/shop.service.js:39:22)
at async /node_modules/@nestjs/core/router/router-execution-context.js:46:28 at async /node_modules/@nestjs/core/router/router-proxy.js:9:17
다행히도 이와 비슷한 경험을 겪었던 팀원이 있어, 빠르게 해결할 수 있었다.
기본적으로 nestjs의 controller는 배치 순서에 영향을 받는다.
문제가 생겼던 controller 메서드를 아래와 같이 변경하였다.
@ApiOperation({ summary: '매장 단건 조회', description: '매장 단건 조회' })
@ApiOkResponse({ type: ShopDto })
@Get('/:shopId')
getShopByShopId(@Param('shopId') shopId: number): Promise<ShopDto> {
return this.shopService.getShopByShopId(shopId);
}
//...
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: '주문 목록 조회', description: '사용자가 생성한 주문 목록 조회' })
@ApiOkResponse({ type: OrderDto, isArray: true })
@Get('/orders')
getOrders(@Query() pageOption: OffsetPaginationOption, @Auth() user: User): Promise<PageResponse<OrderDto>> {
const userId = user.id;
return this.shopService.getOrdersByUserId(userId, pageOption);
}
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: '주문 목록 조회', description: '사용자가 생성한 주문 목록 조회' })
@ApiOkResponse({ type: OrderDto, isArray: true })
@Get('/orders')
getOrders(@Query() pageOption: OffsetPaginationOption, @Auth() user: User): Promise<PageResponse<OrderDto>> {
const userId = user.id;
return this.shopService.getOrdersByUserId(userId, pageOption);
}
// ...
@ApiOperation({ summary: '매장 단건 조회', description: '매장 단건 조회' })
@ApiOkResponse({ type: ShopDto })
@Get('/:shopId')
getShopByShopId(@Param('shopId') shopId: number): Promise<ShopDto> {
return this.shopService.getShopByShopId(shopId);
}
눈치가 빠른 개발자라면 이미 알았겠지만, shopId
를 path variable로 받는 api가 먼저 등장하여 /orders
가 getShopByShopId
메서드의 path variable로 들어가게 된 것이었다.
그 다음 날 조금 더 찾아보니, 아래와 같은 github issue를 발견할 수 있었다.
https://github.com/nestjs/nest/issues/4628
조금 더 찾아보니.. 이런 stackoverflow 글도 볼 수 있었다.
https://stackoverflow.com/questions/32603818/order-of-router-precedence-in-express-js
즉 해당 문제는 express를 기본 Http Adapter로 사용하는 nestjs에서 발생하는 문제였고, 이를 방지하기 위해서 endpoint 구성에 더 신경을 써야 하는 부분이었다.
https://docs.nestjs.com/faq/http-adapter
흔히 많이 사용되는 패턴인 main.ts
에서 bootstrap()
메서드를 통해 NestFactory
를 생성할 때 이Http Adapter를 설정할 수 있다.
내가 겪었던 예시처럼 별도로 설정하지 않는다면 기본적으로 express가 http adapter로써 등록된다.
adapter 객체는 http 서버와 상호작용하기 위한 여러 유용한 메서드들을 제공하지만 getInstance()
메서드를 통해서 각각의 라이브러리 인스턴스에 직접적으로 접근할 수도 있다.
nestjs에서 기본적으로 설정되는 Http Adapter인 express에서는 controller 순서에 따라 핸들링되는 메서드가 영향을 받았다.
fastify 라이브러리를 Http Adapter로써 사용했을 때에도 동일한 결과가 나올까 싶어 간단하게 테스트해보았다.
프로젝트를 다시 설정하기는 귀찮으니 nest 기본 fastify 적용 예시를 아주 살짝 고쳐보았다.
https://github.com/nestjs/nest/tree/master/sample/10-fastify
/src/cats/cats.controller.ts
파일을 아래와 같이 변경하여 테스트 해보았다.
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { Roles } from '../common/decorators/roles.decorator';
import { RolesGuard } from '../common/guards/roles.guard';
import { ParseIntPipe } from '../common/pipes/parse-int.pipe';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './interfaces/cat.interface';
@UseGuards(RolesGuard)
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
@Get(':id')
findOne(
@Param('id', new ParseIntPipe())
id: number,
) {
// get by ID logic
}
@Get('/test')
testRoutePostfix() {
return "test";
}
}
위는 express였다면 /cats/test
를 호출했을 때 findOne()
메서드가 요청을 핸들링했을 것이다.
테스트해본 결과 /cats/test
를 호출하여도 예상했던 것과 같이 testRoutePostfix()
메서드가 요청을 핸들링함을 알 수 있었다.
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { Roles } from '../common/decorators/roles.decorator';
import { RolesGuard } from '../common/guards/roles.guard';
import { ParseIntPipe } from '../common/pipes/parse-int.pipe';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './interfaces/cat.interface';
@UseGuards(RolesGuard)
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
@Get('/test')
testRoutePostfix() {
return "test";
}
@Get(':id')
findOne(
@Param('id', new ParseIntPipe())
id: number,
) {
// get by ID logic
}
}
순서를 위와 같이 변경하여도 결과는 동일했다.
잠깐의 조사를 통해 아래의 사항들을 알 수 있었다.
1. nestjs에서의 기본 Http Adapter는 express가 된다.
2. express에서 route 별 핸들링하는 메서드는 순서에 영향을 받는다.
때문에 조금 생각해보면 이런 개선 방법을 낼 수 있겠다.
/orders
가 공통인 api들은 별도의 controller로 분리하는 방법이 있겠다.