본문 바로가기
TypeScript

[Nest.js] swagger ApiResponse 에서 generic dto 사용하기

by yonikim 2024. 11. 14.
728x90

 

백엔드 개발자로서 API 를 만들다 보면 `GET` 요청에 따른 response 에 pagination, count 등과 같이 공통으로 들어가야 하는 필드들을, 각 dto 마다 한땀한땀 추가해주며 고통받았던 기억이 있을 것이다. 

 

만약 이 표준 응답에 새 필드를 추가해줘야 한다면?

또 모든 dto 를 수동으로 업데이트해줘야 할 것이고, 어느덧 샷건 치고 있는 내 자신을 발견할 수 있다.

 

 

generic 을 적용해보자!

이 고통을 타개할 방법이 무엇이 있을까 생각해보다가 generic 을 적용해보기로 했다. 

import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsNumber } from 'class-validator';

class BaseResponseDTO<T> {
  @ApiProperty({
    description: 'Numbers of returend items',
    example: 2,
  })
  @IsNumber()
  count: number;
    
  pagination: {
    @ApiProperty({ description: '총 결과수' })
    @IsNumber()
    readonly totalRow: number;

    @ApiProperty({ description: '현 페이지의 결과수' })
    @IsNumber()
    readonly pageRow: number;

    @ApiProperty({ description: '다음 페이지 있는지 여부' })
    @IsBoolean()
    readonly hasNext: boolean;

    @ApiProperty({ description: '총 페이지수' })
    @IsNumber()
    readonly totalPage: number;

    @ApiProperty({ description: '현 페이지' })
    @IsNumber()
    readonly page: number;

    @ApiProperty({ description: '요청한 페이지당 사이즈' })
    @IsNumber()
    readonly size: number;
  };
    
  @ApiProperty({
    type: [T],
  })
  list: T[];
}

 

 

완벽한 코드라고 생각했으나, 안타깝게도 이 코드는 동작하지 않았다.

`nestjs/swagger` 는 TypeScript 리플렉션 기능을 사용하는데, 이는 generic 과 함께 동작하지 않는다고 한다.

 


 

Mixins 클래스를 사용해보자!

나의 고통은 영원히 지속되는 것인가 하는 생각에 괴로워하고 있던 찰나, 구세주 글을 발견했다.

 

Mixin 패턴은 특정 기능이나 속성을 여러 클래스에 공통으로 추가할 때 사용하는 디자인 패턴으로, NestJS 에서는 이 Mixin 패턴을 통해 generic 을 사용하는 경우나 반복되는 공통 속성을 쉽게 추가할 수 있다.

역시 믿고 있었다고, NestJS!

 

 

▷ response.dto.ts

import { mixin } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsNumber } from 'class-validator';

class PaginationResponse {
  @ApiProperty({ description: '총 결과수' })
  @IsNumber()
  readonly totalRow: number;

  @ApiProperty({ description: '현 페이지의 결과수' })
  @IsNumber()
  readonly pageRow: number;

  @ApiProperty({ description: '다음 페이지 있는지 여부' })
  @IsBoolean()
  readonly hasNext: boolean;

  @ApiProperty({ description: '총 페이지수' })
  @IsNumber()
  readonly totalPage: number;

  @ApiProperty({ description: '현 페이지' })
  @IsNumber()
  readonly page: number;

  @ApiProperty({ description: '요청한 페이지당 사이즈' })
  @IsNumber()
  readonly size: number;
}

type Constructor<T = object> = new (...args: any[]) => T;

export function withListResponse<T extends Constructor>(Base: T) {
  class BasicPaginationResDto {
    @ApiProperty({
      nullable: false,
      type: [Base],
    })
    list!: T[];

    @ApiProperty({ description: '페이지' })
    readonly pagination: PaginationResponse;
  }

  return mixin(BasicPaginationResDto);
}

 

 

▷ post.controller.ts

import { withListResponse } from './libs/dto/response.dto';
import { PostDto } from './dto/post.dto';
import { PostService } from './post.service';

@ApiTags('API')
@Controller('post')
export class PostController {
  constructor(private readonly service: PostService) {}

  @ApiOperation({ summary: 'post list' })
  @ApiOkResponse({
    type: () => withListResponse(PostDto),
  })
  @Get()
  async list() {
    return this.service.list();
  }
}

 

 

 

References 

https://www.inextenso.dev/how-to-generate-generic-dtos-with-nestjs-and-swagger

 

 

728x90