NestJS response serialization with groups

In this blog post we will find out how we can manipulate the response data in NestJS with serialization groups.

Objective

Learn how to expose or hide response data with the help of NestJS framework and class-transformer package.

Test application

I created a small test application with a User module which i will use to demonstrate group serialization.

Our User entity consists of 6 properties: id, firstName, lastName, username, email and isActive flag. User entity (user.entity.ts):

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { Expose } from "class-transformer";

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column()
  username: string;

  @Column()
  email: string;

  @Column({ default: true })
  isActive: boolean;
}

Our service has two methods, findAll() which retrieves all Users and findOne(id: number) which retrieves a single User by id. User service (user.service.ts):

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  public async findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  public async findOne(id: number): Promise<User> {
    return this.usersRepository.findOneOrFail(id);
  }
}

Controller exposes two endpoints: @Get('users') and @Get('users/:id'). User controller (user.controller.ts):

import {
  Controller,
  Get,
  Param,
  ParseIntPipe,
  SerializeOptions,
  UseInterceptors
} from '@nestjs/common';
import { UserService } from "./user.service";

@Controller()
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get('users')
  public async getUsers(): Promise<User[]> {
    return await this.userService.findAll();
  }

  @Get('users/:id')
  public async getUser(
    @Param('id', ParseIntPipe) userId: number,
  ): Promise<User> {
    return await this.userService.findOne(userId);
  }
}

Response serialization

Currently both requests GET /users and GET /users/:id would return the same response. /users would return a list of Users while /users/:id would return a single User. I would like to return only three properties id, firstName and lastName when we are retrieving all Users and for retrieving a single User i want to return every field except isActive flag.

Currently GET /users returns the following response:

[
  {
    "id": 1,
    "firstName": "John",
    "lastName": "Doe",
    "username": "john.doe",
    "email": "john.doe@example.com",
    "isActive": true
  },
  {
    "id": 2,
    "firstName": "Luka",
    "lastName": "Doncic",
    "username": "luka.doncic",
    "email": "luka.doncic@example.com",
    "isActive": true
  },
  {
    "id": 3,
    "firstName": "LeBron",
    "lastName": "James",
    "username": "lebron.james",
    "email": "lebron.james@example.com",
    "isActive": false
  },
  {
    "id": 4,
    "firstName": "Steph",
    "lastName": "Curry",
    "username": "steph.curry",
    "email": "steph.curry@example.com",
    "isActive": true
  },
  {
    "id": 5,
    "firstName": "Kevin",
    "lastName": "Durant",
    "username": "kevin.durant",
    "email": "kevin.durant@example.com",
    "isActive": true
  }
]

A request to GET /users/1returns the following response:

{
  "id": 1,
  "firstName": "John",
  "lastName": "Doe",
  "username": "john.doe",
  "email": "john.doe@example.com",
  "isActive": true
}

We have to make two changes to our application in order to start utilizing serialization by groups. Firstly we need to add a decorator @Expose() to each property that we want to include in the response. I don't want to return isActive property in any case, that is why i am using @Exclude() decorator which removes this property on serialization. This is how our User entity looks now:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { Exclude, Expose } from "class-transformer";

export const GROUP_USER = 'group_user_details';
export const GROUP_ALL_USERS = 'group_all_users';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  @Expose({ groups: [GROUP_USER, GROUP_ALL_USERS] })
  id: number;

  @Column()
  @Expose({ groups: [GROUP_USER, GROUP_ALL_USERS] })
  firstName: string;

  @Column()
  @Expose({ groups: [GROUP_USER, GROUP_ALL_USERS] })
  lastName: string;

  @Column()
  @Expose({ groups: [GROUP_USER] })
  username: string;

  @Column()
  @Expose({ groups: [GROUP_USER] })
  email: string;

  @Column({ default: true })
  @Exclude()
  isActive: boolean;
}

We added @Expose() decorator with an object with a groups property. We define an array of groups which we want to use.

Secondly we have to add a @SerializeOptions() decorator on each endpoint in our controller. In order that NestJS starts using our Serialization groups we have to add @UseInterceptors(ClassSerializerInterceptor) decorator to the top of our controller. Here is our controller with added decorators:

import {
  ClassSerializerInterceptor,
  Controller,
  Get,
  Param,
  ParseIntPipe,
  SerializeOptions,
  UseInterceptors
} from '@nestjs/common';
import { UserService } from "./user.service";
import { GROUP_ALL_USERS, GROUP_USER, User } from "./user.entity";

@Controller()
@UseInterceptors(ClassSerializerInterceptor)
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get('users')
  @SerializeOptions({
    groups: [GROUP_ALL_USERS],
  })
  public async getUsers(): Promise<User[]> {
    return await this.userService.findAll();
  }

  @Get('users/:id')
  @SerializeOptions({
    groups: [GROUP_USER],
  })
  public async getUser(
    @Param('id', ParseIntPipe) userId: number,
  ): Promise<User> {
    return await this.userService.findOne(userId);
  }
}

Final result

GET /users now returns the following response:

[
  {
    "id": 1,
    "firstName": "John",
    "lastName": "Doe"
  },
  {
    "id": 2,
    "firstName": "Luka",
    "lastName": "Doncic"
  },
  {
    "id": 3,
    "firstName": "LeBron",
    "lastName": "James"
  },
  {
    "id": 4,
    "firstName": "Steph",
    "lastName": "Curry"
  },
  {
    "id": 5,
    "firstName": "Kevin",
    "lastName": "Durant"
  }
]

GET /users/1now returns the following response:

{
  "id": 1,
  "firstName": "John",
  "lastName": "Doe",
  "username": "john.doe",
  "email": "john.doe@example.com"
}

Conclusion

We learned how to apply custom Serialization groups to our entities in order to provide / hide properties that we don't want to expose to clients that are using our API.