How To Properly Serialize API Responses in NestJS

How To Properly Serialize API Responses in NestJS

Imagine you have a public endpoint to get information about a stored user in your database. Of course you don't want to include the user's password. How to serialize the HTTP response to exclude the user's password from the response? If you think you know how to do it, please hang around because I will show you an approach better than the documentation's recommendation. To answer this question, let's recap what happens when we make an HTTP request to some endpoint in NestJS.

Assuming we are using TypeORM, when a request comes to an endpoint like users/1, NestJS will use the Repository to create an instance of the User Entity class. Then when we return that instance, NestJS automatically converts the instance object to JSON.

The Documentation's Solution

The official NestJS documentation recommends we add the @Exclude() decorator on the User's Entity in order to exclude the properties from the response when NestJS creates the instance from the User Entity

This method works fine, but the downside to it is that it will be excluded from every response. What if we want to exclude the password or any other property from the responses of only specific endpoints, like when you have a permission system where some roles have access to more properties than other roles? This method won't work.

My Approach: Creating a Serializing Interceptor

The way I do it is more inclusive and general as follows:

  1. Create an interceptor

  2. Use a function called plainToInstance from the class-transformer package to convert the incoming data to a pre-defined DTO where unwanted properties are excluded

  3. Return that serialized object to the user.

Here is how to implement it:

/* 
 src/interceptors/serialize.interceptor.ts 
*/
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { UserDto } from 'src/users/dtos/User.dto';

@Injectable()
export class SerializeInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data: any) => {
        console.log(data) // UserEntity
        // Transforms the user's instance coming from the Repository
        // to an instance of the User DTO
        const serializedData = plainToInstance(UserDto, data, {
          excludeExtraneousValues: true,
        });
        console.log(serializedData) // UserDto
        return serializedData; // JSON
      }),
    );
  }
}

And here is the User DTO where we can implement any serialization logic on the DTO's properties:

// src/users/dtos/User.dto
import { Expose } from 'class-transformer';

export class UserDto {
  @Expose()
  id: number;

  @Expose()
  email: string;
}

Now the interceptor can be used normally like described in the documentation to do its job:

@UseInterceptors(SerializeInterceptor)
@Controller('users')
export class UsersController {
    // Different endpoints
}

But there is one issue here. Our interceptor works only with a specific DTO. We need to find a way to customize it to work with any DTO. How to achieve that?

Customizable Interceptor for Different DTOs

To make it reusable, we simply define a constructor method to accept any DTO like this:

import {
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  UseInterceptors,
} from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { UserDto } from 'src/users/dtos/User.dto';

interface ClassConstructor<T = any> {
  new (...args: any[]): T;
}

export class SerializeInterceptor implements NestInterceptor {
  constructor(private dto: ClassConstructor) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data: any) => {
        const serializedData = plainToInstance(this.dto, data, {
          excludeExtraneousValues: true,
        });
        console.log(serializedData);
        return serializedData;
      }),
    );
  }
}

Now we can define it like this:

@UseInterceptors(new SerializeInterceptor(dto))
@Controller('users')
export class UsersController {
    // Different endpoints
}
💡
Please note here we remove the @Injectable() decorator because NestJS will think that the dto argument is a dependency to be injected while it's just an interface to define how the interceptor should be defined.

To take the customization of the interceptor to the next level, we can define a custom decorator, which is simply a function, instead of just a custom interceptor:

interface ClassConstructor<T = any> {
  new (...args: any[]): T;
}

export function Serialize(dto: ClassConstructor) {
  return UseInterceptors(new SerializeInterceptor(dto));
}

And now we can use it in our controllers like this:

@Serialize(UserDto)
@Controller('users')
export class UsersController {
    // Different endpoints
}

That's it! Now we have a customized serialization interceptor that works with any DTO where we can define different serialization logic for different routes instead of adopting the fixed approach of defining the @Exclude() decorator inside the entity class.