Dynamically Customize API Response In NestJs


Do you ever create a function to transform your entity into a DTO class? how do you handle transforming entity into a response and make sure to not expose sensitive data?
Maybe, some people will creating a function named mapToDTO on their services. Each service, each function. Well, there is nothing wrong with this solution. But, let’s imagine you are building a backend service using backend for frontend (BFF) pattern. You wasting so much time just to throw your response into a specific client, because you need to specified a function and call it before you sending the response.
Interceptor
In NestJS, there is Interceptor which is a class that will be executed before / after execution. So, at this point you got the AHA Moment, right. Yup, we are going to using interceptor layer to transform object into a DTO. Using the magical class-transformer package, we can decide which field is exposed or excluded, or you want to transform some specific field and do some calculation or something. We can achieve that.
Okay, let’s jump into the execution.
First of all, i assume you have initiate your NestJS project. We will make an API to retrieve users data.
Okay, first thing first, we need to describe our user entity.
// user.entity.ts
export class User {
id?: string;
email: string;
firstname: string;
lastname?: string;
emailVerifiedAt?: Date;
password: string;
createdAt?: Date;
updatedAt?: Date;
deletedAt?: Date;
}
Let’s make our dummy service, this service will have a function to return list of dummy users. We’ll be using faker to generate some fake data.
// user.service.ts
import { Injectable } from '@nestjs/common';
import { User } from './entities/user.entity';
import { faker } from '@faker-js/faker';
const USER: User[] = [
{
id: faker.string.uuid(),
email: faker.internet.email(),
firstname: faker.person.firstName(),
lastname: null,
password: 'password',
emailVerifiedAt: null,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
},
{
id: faker.string.uuid(),
email: faker.internet.email(),
firstname: faker.person.firstName(),
lastname: null,
emailVerifiedAt: new Date(),
password: 'password',
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
},
{
id: faker.string.uuid(),
email: faker.internet.email(),
firstname: faker.person.firstName(),
lastname: faker.person.lastName(),
emailVerifiedAt: new Date(),
password: 'password',
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
},
];
@Injectable()
export class UserService {
public async listUser(): Promise<User[]> {
return Promise.resolve(USER);
}
}
Next thing is, we need to create a controller to handle http request.
// user.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('/v1/user')
export class UserController {
constructor(private readonly userService: UserService) { }
@Get()
getUser() {
return this.userService.listUser();
}
}
Register your service and controller into a module, then run our app using command npm run start:dev. Hit the endpoint localhost:3000/v1/user, we got response object like this.
[
{
"id": "fb44efed-1bbf-4885-aa29-88d48c58c534",
"email": "Danial.Cummerata@gmail.com",
"firstname": "Jaime",
"lastname": null,
"password": "password",
"emailVerifiedAt": null,
"createdAt": "2024-06-27T07:54:36.807Z",
"updatedAt": "2024-06-27T07:54:36.807Z",
"deletedAt": null
},
{
"id": "f4932b4f-e0d3-48ee-a811-774de8035c3f",
"email": "Schuyler67@yahoo.com",
"firstname": "Ally",
"lastname": null,
"emailVerifiedAt": "2024-06-27T07:54:36.807Z",
"password": "password",
"createdAt": "2024-06-27T07:54:36.807Z",
"updatedAt": "2024-06-27T07:54:36.807Z",
"deletedAt": null
},
{
"id": "9ffc8610-d895-481f-a8be-31d51019bead",
"email": "Sarai35@yahoo.com",
"firstname": "Ari",
"lastname": "Oberbrunner",
"emailVerifiedAt": "2024-06-27T07:54:36.808Z",
"password": "password",
"createdAt": "2024-06-27T07:54:36.808Z",
"updatedAt": "2024-06-27T07:54:36.808Z",
"deletedAt": null
}
]
Seriously? password on your response? and even deletedAt field. Huft, no way we can push that thing on git. This time, we need to transform our response using interceptor and plainToInstance. But, before jump into making an interceptor, we need to create a decorator to set our method handler DTO, using SetMetaData from NestJS.
import { SetMetadata } from '@nestjs/common';
export const RESP_DTO_KEY = 'resp-dto';
export const UseRespDTO= (dtoClass: any) =>
SetMetadata(RESP_DTO_KEY, dtoClass);
Attach that decorator on our method handler. To achieve that, we need to update our controller and create a DTO as a response object, we will name it ListUserDTO.
// user.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UserService } from './user.service';
import { UseRespDTO } from 'src/application/common/decorators/set-resp-dto.decorator';
import { ListUserDto } from './dto/list.dto';
@Controller('/v1/user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
@UseRespDTO(ListUserDto)
getUser() {
return this.userService.listUser();
}
}
And, here is the DTO
import { Expose, Transform } from 'class-transformer';
import { isEmpty } from 'class-validator';
export class ListUserDto {
@Expose()
id: string;
@Expose()
@Transform(({ obj }) => `${obj.firstname} ${obj?.lastname ?? ''}`.trim())
fullname: string;
@Expose()
email: string;
@Expose()
@Transform(({ obj }) => isEmpty(obj?.emailVerifiedAt))
is_email_verified: boolean;
@Expose({
name: 'createdAt',
})
created_at: Date;
}
As you can see, our DTO using some decorator from class-transformer. Here’s the short explanation. Our goal is return a user object to be like this.
{
"id": "uuid",
"fullname": "firstname lastname",
"email": "mail@mail.com",
"is_email_verified": true,
"created_at": "2024-06-27T08:20:18.101Z"
}
So, we need to transform is_email_verified by getting a condition of emailVerifiedAt is empty or not. fullname as well, we need to merge firstname and lastname.
The next step is, we need to create our interceptor.
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { map, Observable } from 'rxjs';
import { RESP_DTO_KEY } from '../decorators/set-resp-dto.decorator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class ObjectFormatterInterceptor implements NestInterceptor {
constructor(private readonly reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const dto = this.reflector.get(RESP_DTO_KEY, context.getHandler());
return next.handle().pipe(
map((data) => {
let resp: Object | Array<any> = data;
if (dto) {
resp = {
data: plainToInstance(dto, data, {
strategy: 'excludeAll',
exposeUnsetFields: true,
}),
status: true,
message: 'success',
};
}
return resp
}),
);
}
}
constructor(private readonly reflector: Reflector) This line will injecting Reflector to this interceptor.
const dto = this.reflector.get(RESP_DTO_KEY, context.getHandler()) After that, we need to retrieve our DTO that already been set in controller using UseRespDTO decorator.
plainToInstance(dto, data) This is the key of this tutorial. We are using some options that are provided in plainToInstance function, explore the class-transformer docs to know more about this function.
Yup, here we go. Let’s see our endpoint response.
{
"status": true,
"message": "success",
"data": [
{
"is_email_verified": true,
"id": "168adaaf-6893-45c9-98e7-6cf25dbc0fbf",
"fullname": "Dee",
"email": "Chelsea.Jast@yahoo.com",
"created_at": "2024-06-30T15:39:33.835Z"
},
{
"is_email_verified": false,
"id": "df6f7c8b-f860-4973-a240-ce5738fdf4e7",
"fullname": "Tristin",
"email": "Arne54@yahoo.com",
"created_at": "2024-06-30T15:39:33.835Z"
},
{
"is_email_verified": false,
"id": "5a8a91bb-da9c-4fbc-ba8d-c47a996f572e",
"fullname": "Rosie Hilll",
"email": "Prince6@gmail.com",
"created_at": "2024-06-30T15:39:33.835Z"
}
]
}
Sweet.
Okay, now we learn that we can do many thing to transform a response object using SetMedata and NestJS interceptor. Maybe you can explore more thing like return some message to response or code, even logging a request and response.
Subscribe to my newsletter
Read articles from Taufik Rahadi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Taufik Rahadi
Taufik Rahadi
Hey there! I'm your friendly neighborhood software engineer, fueled by strong coffee, epic tunes, and an unending quest for clean code (and cat cuddles, obviously). When I'm not busy wrangling GraphQL, REST, gRPC, and microservices into submission, you can usually find me whipping up something delicious in the kitchen for my amazing wife or diving headfirst into the wild world of AI. I'm all about pushing the boundaries of what's possible in tech and sharing every 'aha!' moment along the way. Stick around, it's gonna be a fun ride.