How to map REST API data using Decorator pattern in Angular 6

How to map REST API data using Decorator pattern in Angular 6

Sunny Sun Lv4

A cleaner, loosely coupled, and more maintainable solution

To build a solid RESTful API, there are well-established best practices and design patterns we can use. In this article, I will talk about how to use the Decorator pattern to consume RESTful API in Angular.

HttpClient Service

To consume a REST API in Angular, we can use HttpClient . HttpClient Service handles the JSON data parsing under the cover and returns an Rxjs observable. It also supports generic typing functionality. For example, in the code snippet below, getAllTodos will return an observable with an array of objects that match the type TodoModel.

1
2
3
4
5
constructor(private http: HttpClient) {}

getAllTodos(): Observable<TodoModel[]> {
return this.http.get<TodoModel[]>(this.url);
}

It is straightforward to make use of HttpClient to call the REST API and get the data to bind to the view.

The Problem

All seems good so far, but problems start to arise when the back end changes. Let’s say we have the TodoModel as below.

1
2
3
4
5
export class TodoModel{
public name: string;
public id: number,
public completed: boolean,
}

Two changes are required

  1. We want to display a text description of “completed” when the value is true and display “pending” if the value is false.

  2. The field name is changed from “name” to “title”.

The quick way to meet the requirements is to change the model.

1
2
3
4
5
export class TodoModel{
public title: string;
public id: number,
public completed: string,
}

Then change the todo service to map the completed field.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
getAllTodos(): Observable<TodoModel[]> {
return this.http.get(this.url).pipe(
map((data: any[]) => data.map((item: any) => {
const model = new TodoModel();
Object.assign(model, item);
if(item.completed){
model.completed = 'completed';
}else{
model.completed = 'pending';
}
return model;
}))
);
}

In the end, we also need to change the binding at the view to “title” instead of “name”.

It works, but not ideal.

Firstly, it is a simple change: rename one field in the backend. But this simple change requires updating the service, domain model, and view, it is an indication of bad design. Secondly, we may need to repeat the explicit mapping of data to every method that needs to retrieve TodoModel from the back end, which will pollute the code in the long term.

Decorator Pattern

One way to avoid the above problem is to use the Decorator pattern.

Decorator pattern allows you to change the behavior of an object, without changing the original object. This is done by wrapping the original object with a decorator that implements the same interface but adds behavior and/or modifies input and output.

As the diagram illustrates, for the Decorator Pattern, we are not using inheritance to add functionality. Instead, we use composition. It is a more loose coupled pattern.

In Typescript, the decorator is simply a function and will be called at run time. Different types of Decorators can be applied to different levels like Class Decorator, Method Decorator, or Property Decorator.

Here, we use a custom Property Decorator to handle the REST API Data mapping.

StatusConverter Decorator

Here is how we use the “statusConverter” decorator to handle transforming the “complete” status from Boolean to string.

A property decorator takes two arguments:

  • target: the prototype of the class

  • key: the name of the property

The below decorator function contains a getter and setter accessors, so we can manipulate the property value as required.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function statusConverter(target: any, key: string)
{
var value;
if (delete target[key]) {
Object.defineProperty(target, key, {
get: function() {
if (this.value)
{
return 'Completed';
}
else
{
return 'Pending';
}
},
set: function(v) { this.value = v; },
enumerable: true,
configurable: true
});
}
}

A property decorator is declared just before a property decoration.

1
2
@statusConverter
public completed: string;

That is it, no need to manually set the property value in the service code, and We can apply the same decorator everywhere when necessary.

In this case, the use of the statusConverter decorator is very limited, it only converts the true value to “Completed” and the false to “Pending”. However, it is for illustration only, there is no limitation on how the data can be transformed.

PropertyMap Decorator

To handle the case of backend data field renaming, we created the PropertyMap decorator and ModelMapper utility class.

The PropertyMap decorator provides a way to store the meta-data.

To consume the metadata, ModelMapper class is added.

Now we have a ModelMapper class that can take any type, extract the predefined metadata, and performs data transformation.

To use the decorator

1
2
@propertyMap('title')
public name: string;

The updated service looks like the one below.

1
2
3
4
return this.http.get<TodoModel[]>(this.url).pipe(
map(data => data.map((item: any) => {
return new ModelMapper(TodoModel).map(item);
})));

Further refactoring

After we start to make use of the ModelMapper for the services, we will soon find that the code is duplicated in every service. Thus we can extract the modelMapper to a generic API Service class as the code snippet below.

1
2
3
4
5
6
7
8
9
10
// api.service.ts
public get<T>(url: string, itemType: any): Observable<T> {
return this.http.get<T>(this.url).pipe(
map(data => data.map((item: any) => {
return new ModelMapper(itemType).map(item);
})));
}

//todo.service.ts
return this.api.get<TodoModel[]>(this.url, TodoModel);

Benefits

With the above decorators, when the back-end data field is renamed or changed, there is no need to change the service code. The only change required is to update the PropertyMap decorator meta-data argument in the model class.

No matter how many services consume the TodoModel API, only a single change is required to apply it to all services.

The code also becomes more readable as the name of the decorator shows the intent of the function clearly.

Be declarative

Using the Decorator pattern is a practice of declarative programming, it focuses on how the components/services want to behave instead of how to accomplish the result. The declarative approach helps developers to produce a cleaner, loosely coupled, and more maintainable code base.

The full demo code is available at this stackblitz project .

Happy Programming!

  • Title: How to map REST API data using Decorator pattern in Angular 6
  • Author: Sunny Sun
  • Created at : 2018-12-11 00:00:00
  • Updated at : 2024-08-16 19:46:17
  • Link: http://coffeethinkcode.com/2018/12/11/how-to-map-restful_api-using-decorator/
  • License: This work is licensed under CC BY-NC-SA 4.0.