When writing APIs, you'll most likely have to return responses to the client. For example, when writing a sign-up API, you may be required to return the created user object to the client.
An important step to take in development is ensuring that only the required amount of data is returned for performance reasons -and that It does not include sensitive information such as passwords that could compromise your users' accounts. This can be achieved by serializing responses before they are returned..
In this tutorial, you'll learn how serialization works in nestjs and how to serialize your responses in nestjs
prerequisites
To follow along seamlessly, this article assumes you have:
some experience building REST APIS with nodejs and nestjs
An understanding of nestjs project structure and terminologies such as decorators, modules, controllers, providers etc
Some experience programming in javascript and typescript
How nestjs serialization works
serialization in nestjs is enabled using the ClassSerializerInterceptor.
It intercepts the responses and serialises them to the desired format before returning them to the client. The ClassSerializerInterceptor
interceptor can be set at the application level to enable serialization by controllers -or at the service level for serialization by services.
An entity class (serializer class) is created to define the shape of the data one would like to return to the client. The class transformer package is then used to provide a set of rules to transform the data before it is returned to the client. The serialized data becomes an instance of the entity class and the returned value of a method handler as shown in the example below.
Example.
This example is a citizen registration app where citizens signup to become users by providing their information. It demonstrates serializing data by excluding some information provided such as password
and includes the citizenStatus
-a computation derived from the provided date of birth.
Set up:
To get started, in your terminal, install nestjs CLI if you do not have it installed already
npm install -g @nestjs/cli
Use the nest CLI to generate a new project boilerplate and select the package manager you'll be using
nest new citizen_app
next, install some packages that will be required for this example
npm i --save class-validator class-transformer
In the root of your project, create a userData.json file to serve as data storage for this project
touch userData.json
Rest Endpoints:
With the set-up complete, regenerate a resource for the user and select generate REST endpoints
nest g resource user
A new user folder should be added to your src folder containing, service, controllers, dto, entities etc
In the create-user.dto.ts
, define the properties that will be used to create the user. Add some validations to the properties using the class-validator package as shown below:
import { IsNotEmpty, IsString, IsDateString, IsEmail } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty() //should not be empty
@IsString() //should be a string
firstname: string;
@IsNotEmpty()
@IsString()
lastname: string;
@IsDateString({ strict: true }) //should be in format 2022-07-15.
dateOfBirth: Date;
@IsEmail() //should be a valid email
email: string;
@IsNotEmpty()
@IsString()
password: string;
You have to enable global validations in your src/main.ts
bootstrap()
function for the validations to be applied. Add this code just before your app.listen()
app.useGlobalPipes(new ValidationPipe())
In the user
folder, create a new folder called interfaces.
create an interface for the created user
export interface IUser {
id: number;
firstname: string;
lastname: string;
dateOfBirth: Date;
email: string;
password: string;
}
In the src/user.service.ts
, the UserService
class will need to have access to the data storage file to read and write to it. To achieve that, The UserService
class has to instantiate by reading the userData.json
file as follows:
import * as fs from 'fs';
import * as path from 'path';
import {Injectable} from '@nestjs/commom';
import { CreateUserDto } from './dto/create-user.dto';
import { IUser } from './interfaces';
@Injectable()
export class UserService {
private userData: IUser[];
constructor() {
fs.promises
.readFile(path.join(__dirname, '../../userData.json'), 'utf-8')
.then((data) => {
this.userData = JSON.parse(data);
})
.catch((err) => {
console.error(err);
});
}
private saveUser() {
fs.promises.writeFile(
path.join(__dirname, '../../userData.json'),
JSON.stringify(this.userData),
);
}
//the rest of the generated methods go here...
}
Still in src/user/user.service.ts
, add some actual implementation to the rest of the method handlers.
//continuation of userService class...
//create new user
create(dto: CreateUserDto) {
const existingUser = this.userData.find((user) => user.email === dto.email);
if (existingUser) throw new ConflictException('user already exists');
const newUser: IUser = {
id: this.userData.length + 1,
firstname: dto.firstname,
lastname: dto.lastname,
dateOfBirth: dto.dateOfBirth,
email: dto.email,
password: dto.password,
};
this.userData.push(newUser); //add new user to userData
this.saveUser();
return newUser;
}
// find all users
findAll() {
return this.userData;
}
// find a user with id
findOne(id: number) {
const user = this.userData.find((user) => user.id === id);
if (!user) throw new NotFoundException('user not found');
return user;
}
// delete user
remove(id: number) {
const userIndex = this.userData.findIndex((user) => user.id === id);
if (userIndex === -1) throw new NotFoundException('user not found');
this.userData.splice(userIndex, 1);
this.saveUser();
}
Note that in a real application, you'll have to secure your passwords before saving them by hashing them using a third-party package such as bcrypt
With the UserService
fully implemented, start the server by running npm run start:dev
in your terminal and send some requests. You can use Postman, Insomnia or any other tool of your choice.
The response from making a POST
request to the user
endpoint should look like this in Postman.
Notice that among other properties returned in the response object, we have password
-which would compromise the security of your users' data. The plan is to have the password excluded from the response object. For this example, the dateOfBirth
property will also be excluded and instead, a citizenStatus
property -indicating whether the user is a child, adult or senior depending on their date of birth will be included.
Serializing the endpoints
The plan is to use the ClassSerializerInterceptor
at the application level so that all the route handlers in the controller that are provided with an instance of the entity class (serializer class) can perform serialization.
In your src/main.ts
, import the ClassSerializerInterceptor
from @nestjs/common
and add the interceptor globally. Your final bootstrap()
function should look like this:
import { ClassSerializerInterceptor } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe()); //set validations to the application level
// 👇 apply transform to all responses
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
await app.listen(3000);
}
bootstrap();
Next, create an entity class and supply a set of rules for transforming the data. Add this to your src/user/entities/user.entity.ts
import { Exclude, Expose } from 'class-transformer';
import { IUser } from '../interfaces';
interface IUserEntity extends IUser {
get citizenStatus(): CitizenStatus;
}
type CitizenStatus = 'Child' | 'Adult' | 'Senior';
export class UserEntity implements IUserEntity {
id: number;
firstname: string;
lastname: string;
email: string;
@Exclude()
dateOfBirth: Date;
@Exclude()
password: string;
@Expose()
get citizenStatus(): CitizenStatus {
const birthDate = new Date(this.dateOfBirth).getFullYear();
const currentYear = new Date().getFullYear();
const age = currentYear - birthDate;
if (age < 18) {
return 'Child';
} else if (age > 18 && age < 60) {
return 'Adult';
} else {
return 'Senior';
}
}
In the provided snippet, the @Exclude()
decorator from the class-transformer package is used to indicate that the password
and dateOfBirth
properties should be excluded from the response objects. On the other hand, the citizenStatus
property is indicated to be included using the @Expose()
decorator.
Since citizenStatus
is not an actual property of the entity class -but rather a computed value based on the dateOfBirth
property, the get
keyword is used to define a getter method for citizenStatus
instead of a regular property.
With the entity class created, the next step is to use it in the controller. Refactor your src/user/user.controller.ts
to match this:
import { Controller, Get, Post, Body, Param, Delete } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UserEntity } from './entities/user.entity';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto): UserEntity {
return new UserEntity(this.userService.create(createUserDto));
}
@Get()
findAll(): UserEntity[] {
const users = this.userService.findAll();
return users.map((user) => new UserEntity(user));
}
@Get(':id')
findOne(@Param('id') id: string): UserEntity {
return new UserEntity(this.userService.findOne(+id));
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.userService.remove(+id);
}
}
The controller's method handlers that will need to be serialized are refactored to return an instance of the entity class with the return values of the delegated service handlers. A return type of UserEntity
is also added.
Restart your application and send some requests to confirm the serialiser is working as expected. The GET
request to fetch all users provides the following response:
From the response, you can tell the data has been properly serialized to the desired format.
Conclusion
If you've made it to the end of this article, you've learnt how serialization works in nestjs and learnt hands-on to serialize data in nestjs. From the example, you can tell that among other things, you can return only a subset of an object and also make use of getters and setters.
There are so many other transformations you can apply to your response objects and this tutorial only scratched the surface with those. To dive deep into transformations, feel free to read more on the class-transformer Github. Also, check out the nestjs docs on this same topic.
The complete source code for this example can be found on Github here
Top comments (1)
Thx for this post I was able to find undestand how this works ❤️