[NestJS + Swagger] Common Response Type 설정

devwuu·2023년 11월 7일

Dailyday

목록 보기
2/2

예제 프로젝트
🏠 https://github.com/devwuu/dailyday

1. 공통된 Response Schema에 대한 고민

interface Response<T> {
  data: T;
}

export class SuccessResponseInterceptor<T>
  implements NestInterceptor<T, Response<T>>
{
  intercept(
    context: ExecutionContext,
    next: CallHandler<T>,
  ): Observable<Response<T>> | Promise<Observable<Response<T>>> {
    return next.handle().pipe(
      map((origin) => ({
        data: origin,
      })),
    );
  }
}

확장성을 고려해 Interface를 이용해서 API의 response 포맷을 공통화하는 것까진 좋았는데 Swagger에 Reponse Type을 적용해주려고 보니 (1) 단순하게 DTO를 적용하면 실제 응답 포맷과 달라지고 (2) 단순한 구조라 DTO를 특별히 만들지 않은 케이스의 경우 type에 걸어줄 DTO가 없다는 문제점이 생겼다.
그렇다고 기껏 만들어둔 공통 포맷을 빼면 reponse에 공통적으로 들어가야 하는 정보가 생겼을 때 모든 DTO를 수정해야하기 때문에 상당히 번거로울 수 밖에 없어질 것 같았다.
또 단순한 구조를 매번 새로운 DTO로 만드는 건... DTO를 지나치게 많이 만드는 데 일조하는 것 같아 조금 마음이 쓰였다.
결국 (1)번과 (2)번 모두 리팩토링이 필요한 상태가 되었다😂 다행히 두 케이스 모두 공식 문서를 통해 해결할 수 있었다.


(1) @ApiResponse에 간단한 Schema 적용하기

  @ApiOperation({
    tags: ['회원'],
    summary: '회원가입',
  })
  @ApiResponse({
    status: 201,
    description: '회원가입 성공',
    // type: ????
  })
  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    const userId = await this.usersService.create(createUserDto);
    return { userId };
  }

(2)번 케이스는 @ApiResponse의 type을 정해줄 수가 없으니 schema를 이용해 직접 정의해줘야했다. schema는 properties 속성으로 하나씩 정의해주면 되는데 이렇게 하나씩 정의하다보면 데코레이터 부분이 길어지기 십상인지라 정말 간단한 구조가 아니면 차라리 DTO로 빼는 게 보기 좋을 것 같았다.

  @ApiOperation({
    tags: ['회원'],
    summary: '회원가입',
  })
  @ApiResponse({
    status: 201,
    description: '회원가입 성공',
    schema: {
      properties: {
        // key
        userId: {
          type: 'string', // value 타입
          description: '생성된 user Id', // value 설명
          example: '075b9be6-6d99-4e08-942d-4e392fef80a7', // 예제값
        },
      },
    },
  })
  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    const userId = await this.usersService.create(createUserDto);
    return { userId };
  }

상기와 같이 정의해주면 아래와 같이 스키마와 예제값까지 확인할 수 있게 된다

(2) 공통 Schema를 Swagger에 적용하기

간단한 스키마를 적용하는 것까진 성공했는데 본격적인 문제가 남아있었다. 공통 스키마를 위와 같은 방법으로 하나씩 적용하기는... 좀 그랬다. DTO가 있어 타입으로 간단하게 정의된 부분까지 다 뜯어서 하나씩 정의하기엔... 굉장히 많이 그랬다. DTO에 있는 속성을 하나씩 다 적어줘야 할지도 모르는 일이니까... 그래서 공통 스키마를 한꺼번에 적용하는 방법을 찾아봤다.
NestJS 공식 문서에선 이 부분을 Pagination을 예제로 들어 설명하고 있었다. (https://docs.nestjs.com/v9/openapi/operations#advanced-generic-apiresponse)

1. custom decorator 생성

import { applyDecorators } from '@nestjs/common';
import { ApiOkResponse } from '@nestjs/swagger';
import {
  ReferenceObject,
  SchemaObject,
} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';

export const ApiCommonResponse = (
  obj: SchemaObject & Partial<ReferenceObject>,
) => {
  return applyDecorators(
    ApiOkResponse({
      schema: {
        properties: {
          data: {
            ...obj,
          },
        },
      },
    }),
  );
};

@ApiReponse를 대신해 사용할 커스텀 데코레이터를 만든다. 공식 문서를 참고해서 아예 properties 부분을 통째로 오버라이딩 할 수 있도록 적용했다. 개별 schema도 적용해야 하고 type도 적용해야 하니까... 파라미터 타입은 개별 property 내부에 정의되는 SchemaObject & Partial<ReferenceObject> 타입으로 제한시켰다.

2. schema 적용

@ApiResponse를 @ApiCommonResponse로 대체해준다. 이때 @ApiResponse에 정의 되었던 스키마를 파라미터로 넣어주면 해당 스키마가 오버라이딩 되는 걸 볼 수 있다.

  @ApiOperation({
    tags: ['회원'],
    summary: '회원가입',
  })
  @ApiCommonResponse({
    properties: {
      userId: {
        type: 'string',
        description: '생성된 user Id',
        example: '075b9be6-6d99-4e08-942d-4e392fef80a7',
      },
    },
  })
  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    const userId = await this.usersService.create(createUserDto);
    return { userId };
  }

@ApiCommonResponse를 조금 더 수정하면 기존에 적용했던 desc도 간단하게 적용할 수 있을 거 같다.

3. DTO 적용

DTO를 스키마에 적용시켜줄 때는 @ApiResponse를 @ApiCommonResponse로 바꿔주는 것 외에 다른 작업이 추가로 필요하다

  @ApiOperation({
    tags: ['일기'],
    summary: '일기와 감정 상세 조회',
    description: '일기와 일기에 등록된 감정 내용을 일기 id로 조회합니다',
  })
  @ApiResponse({
    status: 200,
    description: '일기와 감정 상세 조회 성공',
    type: JournalEmotionDto,
  })
  @Get('id/:id')
  findOneByJournalId(@Param('id') id: string) {
    return this.journalsEmotionsService.findOneByJournalIdWithAllContent(id);
  }

기존에는 type으로 적용되어있던 부분을 $ref로 바꿔주면서 @ApiExtraModels를 추가해줘야 정상적인 참조가 가능하다.

  @ApiOperation({
    tags: ['일기'],
    summary: '일기와 감정 상세 조회',
    description: '일기와 일기에 등록된 감정 내용을 일기 id로 조회합니다',
  })
  @ApiExtraModels(JournalEmotionDto)
  @ApiCommonResponse({
    $ref: getSchemaPath(JournalEmotionDto),
  })
  @Get('id/:id')
  findOneByJournalId(@Param('id') id: string) {
    return this.journalsEmotionsService.findOneByJournalIdWithAllContent(id);
  }

2. 출처

https://docs.nestjs.com/v9/openapi/operations
https://puleugo.tistory.com/116

profile
일단 한다

1개의 댓글

comment-user-thumbnail
2024년 3월 29일

많은 도움이 되었습니다. 감사합니다.

답글 달기