usher
usher

Reputation: 113

Nestjs Response Serialization with array of objects

I want to serialize a controller response by the nestjs serialization technique. I didn't find any approach and my solution is as follows:

User Entity

export type UserRoleType = "admin" | "editor" | "ghost";

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

    @Column('text')
        username: string;
    @Column('text') 
        password: string;
    @Column({
        type: "enum",
        enum: ["admin", "editor", "ghost"],
        default: "ghost"
    })
    roles: UserRoleType;
        @Column({ nullable: true })
                profileId: number;  
}

User Response Classes

import { Exclude } from 'class-transformer';

export class UserResponse {
    id: number;

    username: string;

    @Exclude()
    roles: string;

    @Exclude()
    password: string;

    @Exclude()
    profileId: number;  

    constructor(partial: Partial<UserResponse>) {
        Object.assign(this, partial);
    }
}

import { Exclude, Type } from 'class-transformer';
import { User } from 'src/_entities/user.entity';
import { UserResponse } from './user.response';

export class UsersResponse {

    @Type(() => UserResponse)
    users: User[]   

    constructor() { }
}

Controller

@Controller('user')
export class UsersController {
    constructor(
        private readonly userService: UserService
    ) {

    }
    @UseInterceptors(ClassSerializerInterceptor)
    @Get('all')
    async findAll(
    ): Promise<UsersResponse> {
        let users = await this.userService.findAll().catch(e => { throw new   NotAcceptableException(e) })
        let rsp =new UsersResponse() 
        rsp.users = users
        return rsp
    }

It works, but I must explicitly assign the db query result to the response users member. Is there a better way? Thanks a lot

Here the actual Response and wanted result, for a better explanation.

Result in this Approach

{
  "users": [
    {
      "id": 1,
      "username": "a"
    },
    {
      "id": 2,
      "username": "bbbbbb"
    }
  ]
}

Result Wanted

{
    {
      "id": 1,
      "username": "a"
    },
    {
      "id": 2,
      "username": "bbbbbb"
    }
}

Upvotes: 10

Views: 35967

Answers (4)

Yilmaz
Yilmaz

Reputation: 49301

Your approach is recommended by nestjs but that has a fault. You are excluding some properties from being exposed to the client. What if, you work in a project that has an admin and admin wants to see all the data about the users or products. If you exclude fields in the entities, your admin won't see those fields either. Instead, leave the entities as it is, and write dto's for each controller or for each request handler and in this dto's just list the properties you want to expose.

Then write a custom interceptor and create specific dto for ecah entity. For example in your example, you create a userDto:

import { Expose } from 'class-transformer';

// this is a serizalization dto
export class UserDto {
  @Expose()
  id: number;
  @Expose()
  roles: UserRoleType;
  @Expose()
  albums: Album[];
 // Basically you list what you wanna expose here
}

custom interceptor is a little messy:

import {
  UseInterceptors,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';

// Normally user entity goes into the interceptor and nestjs turns it into the JSON. But we we ill turn it to User DTO which will have all the serialization rules.then nest will take dto and turn it to the json and send it back as response


export class SerializerInterceptor implements NestInterceptor {
    // dto is the variable. so you can use this class for different entities
    constructor(private dto:any){

    }
  intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
   // you can write some code to run before request is handled
    return handler.handle().pipe(
      // data is the incoming user entity
      map((data: any) => {
        return plainToClass(this.dto, data, {
          //   this takes care of everything. this will expose things that are set in the UserDto
          excludeExtraneousValues: true,
        });
      }),
    );
  }
}

Now you use this in the controller:

// See we passed UserDto. for different entities, we would just write a new dto for that entity and our custom interceptor would stay reusable
@UseInterceptors(new SerializerInterceptor(UserDto))
@Get('all')
    async findAll(
    ): Promise<UsersResponse> {
        let users = await this.userService.findAll().catch(e => { throw new   NotAcceptableException(e) })
        let rsp =new UsersResponse() 
        rsp.users = users
        return rsp
    }

Upvotes: 2

MUHAMMAD HIZBULLAH
MUHAMMAD HIZBULLAH

Reputation: 11

I have alternative way for your problem. you can remove @UseInterceptors(ClassSerializerInterceptor) from your Controller. Instead use serialize and deserialize function.

import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';

@Get('all')
async findAll() {
  const users = serialize(await this.userService.findAll());
  return {
     status: 200,
     message: 'ok',
     users: deserialize(User, users)
  };
}

it's work too for single data

import { Param } from '@nestjs/common';    
import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';

@Get(':id')
async findById(@Param('id') id: number) {
  const user = serialize(await this.userService.findById(id));
  return {
    status: 200,
    message: 'ok',
    user: deserialize(User, user)
  };
}

Upvotes: 1

Kim Kern
Kim Kern

Reputation: 60357

I would recommend to directly put the @Exclude decorators on your entity class User instead of duplicating the properties in UserResponse. The following answer assumes you have done so.


Flat Response

If you have a look at the code of the ClassSerializerInterceptor, you can see that it automatically handles arrays:

return isArray
  ? (response as PlainLiteralObject[]).map(item =>
      this.transformToPlain(item, options),
    )
  : this.transformToPlain(response, options);

However, it will only transform them, if you directly return the array, so return users instead of return {users: users}:

@UseInterceptors(ClassSerializerInterceptor)
@Get('all')
async findAll(): Promise<User> {
    return this.userService.findAll()
}

Nested Response

If you need the nested response, then your way is a good solution. Alternatively, you can call class-transformer's serialize directly instead of using the ClassSerializerInterceptor. It also handles arrays automatically:

import { serialize } from 'class-transformer';

@Get('all')
async findAll(): Promise<UsersResponse> {
  const users: User[] = await this.userService.findAll();
  return {users: serialize(users)};
}

Upvotes: 5

usher
usher

Reputation: 113

Wow, what easy, if i know! Perfect, this solves my problem. Also your recommendation for the User Entity with the class-transformer @Exclue() decorator.

And i know that i do not need a custom UsersResponse class in this use case. This solution was that what i was looking for, but i overjump this quite easy way

Thank you so much for your superfast answer and the problem solution.

Greetings to Berlin from Rostock :)

Here my final approach:

Controller

@UseInterceptors(ClassSerializerInterceptor)
@Get('all')
async findAll(
): Promise<User> {
    return await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
}

User Entitiy

import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn, OneToMany } from 'typeorm';
import { Profile } from './profile.entity';
import { Photo } from './photo.entity';
import { Album } from './album.entity';
import { Exclude } from 'class-transformer';

export type UserRoleType = "admin" | "editor" | "ghost";

@Entity()
export class User {
    @PrimaryGeneratedColumn() id: number;
    @Column('text')
    username: string;

    @Exclude()
    @Column('text')
    password: string;

    @Column({
        type: "enum",
        enum: ["admin", "editor", "ghost"],
        default: "ghost"
    })
    roles: UserRoleType;

    @Exclude()
    @Column({ nullable: true })
    profileId: number;

    @OneToMany(type => Photo, photo => photo.user)
    photos: Photo[];

    @OneToMany(type => Album, albums => albums.user)
    albums: Album[];

    @OneToOne(type => Profile, profile => profile.user)
    @JoinColumn()
    profile: Profile;
}

Response Result

[
  {
    "id": 1,
    "username": "a",
    "roles": "admin"
  },
  {
    "id": 2,
    "username": "bbbbbb",
    "roles": "ghost"
  }
]

Upvotes: 1

Related Questions