들어가기
- API 문서화를 위해서 OpenAPI Specification(swagger) 를 많이 사용한다.
- 그렇기에 API 를 만드는 경우 이 문서화는 필수로 해야한다(그렇지 않을 경우 무수한 질문의 요청을 ...)
- nestjs 도 decorator 를 통해 OAS 를 지원하나 이를 위해선 response class 가 필수적이다.
- 문제가 되는 부분은 이 문서화를 위한 response-class 가 강제화가 되며 결과적으로 1개의 API 를 만들고 이를 문서화 하기 위해선 response-class 가 무조건 필요로 하게 된다.
- 여러개의 많은 class 가 발생하다 보니 당연히 이를 재활용하고 싶은 욕구가 생기고 결국 response 간의 커플링이 발생하는 문제 역시 생긴다.
- 실제 문제되는 상황을 찬찬히 봐보자
문제 되는 상황
단순 메시지 전달하기
- 데이터가 생성 되었다를 안내하기 위해서도 response 가 필요로 하다.
class SomeResponse {
@ApiProperty({
example: 'success',
description: 'status of api',
})
status: string;
...
}
...
@ApiOkResponse({type:SomeResponse})
@Get('/sample')
async sample() {
return { status: 'success' };
}
여러개 response 조합하기
- 클라이언트에서 User 와 Setting 정보를 전달해야한다고 하는 경우 이를 swagger 에 같이 보여주기 위해선 이를 확장시킨 class 를 또 만들어줘야한다.
class UserResponse {
@ApiProperty({
example: 'john',
description: 'user nickname',
})
nickname: string;
constructor(nickname: string) {
this.nickname = nickname;
}
}
class SettingResponse {
@ApiProperty({
example: 'setting-name',
description: 'setting-name',
})
name: string;
constructor(name: string) {
this.name = name;
}
}
class SomeResponse {
@ApiProperty({
description: 'user',
type: UserResponse,
})
user: UserResponse;
@ApiProperty({
description: 'user',
type: SettingResponse,
})
setting: SettingResponse;
constructor(user: UserResponse, setting: SettingResponse) {
this.user = user;
this.setting = setting;
}
}
...
@ApiOkResponse({type:SomeResponse})
@Get('/sample')
async sample() {
return new SomeResponse(...)
}
- 위 상황에서 User 와 Account 정보를 전달해야한다고 한다면? 이를 포함하는 또 다른 class를 만들어줘야한다(SomeOtherResponse 같은)
- 결국 내가 원하는 것은 최소한의 core response class 를 만들고 이를 활용하는 것이다.
dynamic 하게 api 를 표현해보자
- 이를 위해선 nest js 에서 제공하는 custom decorator 를 사용하면 된다.
- 아래 예시는 body 안에 'data'라는 key word 를 1차로 감싸는 형태이다.
- 모든 response 가 class(=key) 단위로 사용되기를 원하므로 1차 deapth 를 추가하였다.
- decorator name 는 다소 러프하게 작성되었다.
1. 단순 결과 값 내보내기
import { applyDecorators } from '@nestjs/common';
import { ApiOkResponse } from '@nestjs/swagger';
import { ReferenceObject, SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
export const ApiPropertyResponse = (
obj: SchemaObject & Partial<ReferenceObject>,
) => {
return applyDecorators(
ApiOkResponse({
schema: {
properties: {
data: {
...obj,
},
},
},
}),
);
};
...
@ApiOperation({
summary: 'swagger sample',
description: 'composition of items',
})
@ApiPropertyResponse({
properties: {
status: {
type: 'string',
description: '성공 실패 여부, 단순 참고용',
},
},
})

2. 특정 key 로 감싸기
import { Type, applyDecorators } from '@nestjs/common';
import {
ApiExtraModels,
ApiOkResponse,
ApiProperty,
getSchemaPath,
} from '@nestjs/swagger';
class ObjectResponse<T> {
@ApiProperty({
description: 'composition of response',
})
data: T;
constructor(data: T) {
this.data = data;
}
}
export const ApiObjectResponse = <T extends Type<unknown>>(response: T) =>
applyDecorators(
ApiExtraModels(ObjectResponse, response),
ApiOkResponse({
schema: {
type: 'object',
properties: {
data: { $ref: getSchemaPath(response) },
},
},
}),
);
...
@ApiOperation({
summary: 'swagger sample',
})
@ApiObjectResponse(AccountResponse)
3. 특정 key 를 다이나믹 하게 사용하기
import { Type, applyDecorators } from '@nestjs/common';
import { ApiExtraModels, ApiOkResponse, ApiProperty, getSchemaPath } from '@nestjs/swagger';
class ObjectResponse<T> {
@ApiProperty({
description: 'composition of response',
})
data: T;
constructor(data: T) {
this.data = data;
}
}
export const ApiCompositedObjectResponse = <
T extends Record<string, Type<unknown> | Type<unknown>[]>,
>(
responses: T,
) => {
const properties: Record<string, any> = {};
const extraModels: Function[] = [];
for (const [key, response] of Object.entries(responses)) {
if (Array.isArray(response)) {
properties[key] = {
type: 'array',
items: {
$ref: getSchemaPath(response[0]),
},
};
extraModels.push(...response);
} else {
properties[key] = { $ref: getSchemaPath(response as Type<unknown>) };
extraModels.push(response);
}
}
return applyDecorators(
ApiExtraModels(ObjectResponse, ...extraModels),
ApiOkResponse({
schema: {
type: 'object',
properties: {
data: {
type: 'object',
properties,
},
},
},
}),
);
};
...
@ApiOperation({
summary: 'swagger sample',
description: 'composition of items',
})
@ApiCompositedObjectResponse({
user: UserResponse,
setting: SettingResponse,
settings: [SettingResponse],
})

4. array 로 전달하기
4.1. 1개의 response 가 array 형태인 경우
import { Type, applyDecorators } from '@nestjs/common';
import {
ApiExtraModels,
ApiOkResponse,
ApiProperty,
getSchemaPath,
} from '@nestjs/swagger';
export class ArrayResponse<T> {
@ApiProperty({
description: 'Array of responses',
isArray: true,
})
data: T[];
constructor(data: T[]) {
this.data = data;
}
}
export const ApiArrayResponse = <T extends Type<unknown>>(response: T) =>
applyDecorators(
ApiExtraModels(ArrayResponse, response),
ApiOkResponse({
schema: {
allOf: [
{ $ref: getSchemaPath(ArrayResponse) },
{
properties: {
data: {
type: 'array',
items: { $ref: getSchemaPath(response) },
},
},
},
],
},
}),
);
...
@ApiOperation({
summary: 'swagger sample',
description: 'composition of items',
})
@ApiArrayResponse(UserResponse)

4.2. 여러개의 response 를 1개의 array 로 표현해야하는 경우
import { Type, applyDecorators } from '@nestjs/common';
import {
ApiExtraModels,
ApiOkResponse,
ApiProperty,
getSchemaPath,
} from '@nestjs/swagger';
export class ArrayResponse<T> {
@ApiProperty({
description: 'Array of responses',
isArray: true,
})
data: T[];
constructor(data: T[]) {
this.data = data;
}
}
export const ApiArrayCompositedObjectResponse = <
T extends Record<string, Type<unknown>>,
>(
responses: T,
) => {
const responseValues = Object.values(responses);
return applyDecorators(
ApiExtraModels(ArrayResponse, ...responseValues),
ApiOkResponse({
schema: {
allOf: [
{ $ref: getSchemaPath(ArrayResponse) },
{
properties: {
data: {
type: 'array',
items: {
type: 'object',
properties: Object.fromEntries(
Object.entries(responses).map(([key, response], index) => [
key,
{ $ref: getSchemaPath(response) },
]),
),
},
},
},
},
],
},
}),
);
};
...
@ApiOperation({
summary: 'swagger sample',
})
@ApiArrayCompositedObjectResponse({
user: UserResponse,
setting: SettingResponse,
})

결론
- 이를 통해서 우리는 정말 core 한 형태의 class 만 작성하면 되고 새로운 api 문서화를 위한 class 작업 및 조합 작업들을 할 필요가 없어졌다.
- 진작 공부좀 할껄 ..