You can use interceptor.
Interceptors have a set of useful capabilities which are inspired by the Aspect Oriented Programming (AOP) technique. They make it possible to:
- bind extra logic before / after method execution
- transform the result returned from a function
- transform the exception thrown from a function
- extend the basic function behavior
Nestjs
Here is my own way
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ClassTransformOptions } from '@nestjs/common/interfaces/external/class-transform-options.interface';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Response } from 'express';
import { ClassConstructor, plainToClass } from 'class-transformer';
interface ResponseBody {
statusCode: number;
data: unknown;
}
@Injectable()
export class FormatResponse implements NestInterceptor {
private convertOptions: ClassTransformOptions = {
excludeExtraneousValues: true,
enableImplicitConversion: true,
exposeDefaultValues: true
};
constructor(private reflector: Reflector) {}
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<ResponseBody> {
const response: Response = context.switchToHttp().getResponse();
return next.handle().pipe(
map(res => {
const rdto = this.reflector.get('rdto', context.getHandler());
return {
statusCode: response.statusCode,
data: this.convert(rdto, res)
};
})
);
}
private convert(rdto: ClassConstructor<unknown>, res: unknown) {
return rdto
? Array.isArray(res)
? res.map(el => plainToClass(rdto, el, this.convertOptions))
: plainToClass(rdto, res, this.convertOptions)
: res;
}
}
To define response dto you need to create custom decorator
import { CustomDecorator, SetMetadata } from '@nestjs/common';
export const ResDto = (resDto: unknown): CustomDecorator<string> =>
SetMetadata('rdto', resDto);
Example
Now you can define response DTO for each route.
DTO
import { Expose } from 'class-transformer';
export class AccountRdto {
@Expose()
phone: string;
@Expose()
name: string;
}
export class SignUpRdto extends AccountRdto {
@Expose()
activeCodeExpires: number;
}
route
@Post('/signup')
@ResDto(SignUpRdto)
async signUp(
@Res({ passthrough: true }) res: Response,
@Body() dto: SignUpDto
): Promise<SignUpRdto> {
const user = await this.authService.signUp(dto);
return user;
}