In this blog post we will find out how we can manipulate the response data in NestJS with serialization groups.
Learn how to expose or hide response data with the help of NestJS framework and class-transformer package.
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);
}
}
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/1
returns 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);
}
}
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/1
now returns the following response:
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"username": "john.doe",
"email": "john.doe@example.com"
}
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.