Unlocking the Power of Serialization in Your Applications

The Need for Serialization

When dealing with objects in our applications, we often encounter sensitive information that needs to be protected. For instance, a user object fetched from a database may contain a password that should not be exposed to the end client. Additionally, some information saved in an object may not be useful for the client and should be removed to save bandwidth. This is where serialization comes in – a process of preparing an object to be sent over the network to the end client.

The Limitations of Out-of-the-Box Serialization

NestJS provides a way to serialize objects returned from API endpoints using a decorator and a library called class-transformer. While this solution works for basic cases, it falls short in more complicated scenarios. For example, if we want to create a findAll method that returns multiple user objects, we cannot simply return an array of objects because the serialization mechanism will not work as expected.

Creating a Reusable Serialization Solution

To overcome these limitations, we need to create our own serialization mechanism that provides more flexibility and control. This involves implementing two key components: a “parent” class that every serializer will extend, and an interceptor that will take care of running our serializers.

The Flow of Serialization

Our serialization mechanism will work as follows: each controller marks which properties should be serialized, and then the interceptor goes over the keys of the returned object and serializes the values that were marked. We can “mark” an object by wrapping it into a class called Serializable, which keeps a reference to a function that will be used to serialize a value.

export class Serializable<T> {
  private readonly serializeFn: (value: T) => any;

  constructor(serializeFn: (value: T) => any) {
    this.serializeFn = serializeFn;
  }

  serialize(value: T): any {
    return this.serializeFn(value);
  }
}

BaseSerializerService

To create our base serializer, we will create an abstract class called BaseSerializerService that provides reusable methods for all serializers. This class takes two generic types, E and T, which stand for an entity and a serialized value, respectively. Each serializer will implement its own serialize method, which takes an entity and a user role, and returns a serialized object.

export abstract class BaseSerializerService<E, T> {
  abstract serialize(entity: E, role: string): T;
}

Serialization Methods

Our serialization mechanism will provide several features, including:

  • Asynchronous Serialization: allowing us to handle large datasets efficiently
  • Nested Serialization: serializing nested objects and arrays
  • Adding Additional Properties: adding properties that weren’t in the original object
  • Serializing Collections: serializing collections of entities
  • Wrapping Values: wrapping values in a Serializable class

SerializerInterceptor

To create our interceptor, we will use Nest’s interceptor mechanism, which allows us to transform the object returned from a controller method. Our interceptor will check whether the response is an object, and if so, it will use the serializeResponse method to serialize the object.

export class SerializerInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      tap((response) => {
        if (typeof response === 'object') {
          this.serializeResponse(response);
        }
      })
    );
  }

  private serializeResponse(response: any): void {
    // serialize the response object
  }
}

Real-World Use Case

Let’s consider a real-world use case where we have a user entity that represents an author, and it references many articles that the author wrote. Our goal is to make sure that the password property is removed and the nested articles are also serialized. We can achieve this by using the articleSerializerService to serialize an article instead of writing the same logic in the userSerializerService.

export class UserSerializerService extends BaseSerializerService<User, SerializedUser> {
  constructor(private readonly articleSerializerService: ArticleSerializerService) {
    super();
  }

  serialize(user: User, role: string): SerializedUser {
    const serializedUser: SerializedUser = {
      id: user.id,
      name: user.name,
      articles: user.articles.map((article) => this.articleSerializerService.serialize(article, role)),
    };
    return serializedUser;
  }
}

Leave a Reply