DEV Community

loading...
Cover image for Consuming APIs in Angular – The Model-Adapter Pattern

Consuming APIs in Angular – The Model-Adapter Pattern

Florimond Manca
Pythonista, open source developer, tech blogger. Idealist on a journey, and it’s good fun!
Originally published at blog.florimond.dev Updated on ・8 min read

Originally published at blog.florimondmanca.com on Sep 4, 2018.

In a previous post, I wrote about best practices in REST API design. These were mostly applicable on the server side.

Since January 2018, I've also been doing frontend development with Angular, both as a hobby and professionally.

Today, I want to talk about architecture and share with you a design pattern that has helped me structure and standardise the way I integrate REST APIs with Angular frontend applications.

TL;DR:

Write a model class and use an adapter to convert raw JSON data to this internal representation.

Tip: you can find the final code for this post on GitHub: ng-courses.

Let's dive in!

The problem

What is it that we're trying to solve, exactly?

Most often these days, frontend apps need to interact a lot with external backend services to exchange, persist and retrieve data.

A typical example of this, which we're interested in today, is retrieving data by requesting a REST API.

These APIs return data in a given data format. This format can change over time, and it is most likely not the optimal format to use in our Angular apps built with TypeScript.

So, the problem we're trying to solve is:

How can we integrate an API with an Angular frontend app, while limiting the impact of changes and making full use of the power of TypeScript?

The course subscription system

As an example, we'll consider a course subscription system in which students can apply to courses and access learning material.

Here's the user story for the feature we're trying to build:

"As a student, I want to view the list of courses so that I can register to new courses."

To allow this, we have been provided with the following API endpoint:

GET: /courses/
Enter fullscreen mode Exit fullscreen mode

Its response data (JSON-formatted) looks something like this:

[
    {
        "id": 1,
        "code": "adv-maths",
        "name": "Advanced Mathematics",
        "created": "2018-08-14T12:09:45"
    },
    {
        "id": 2,
        "code": "cs1",
        "name": "Computer Science I",
        "created": "2018-06-12T18:34:16"
    }
]
Enter fullscreen mode Exit fullscreen mode

Why not just JSON.parse()?

The quick-and-dirty solution would be to make HTTP calls, apply JSON.parse() on the response body, and use the resulting Object in our Angular components and HTML templates. As it turns out, Angular's HttpClient can do just this for you, so we wouldn't even have to do much work.

In fact, this is how simple a CourseService doing this HTTP request would be:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
    providedIn: 'root'
})
export class CourseService {

    constructor(private http: HttpClient) {}

    list(): Observable<any> {
        const url = 'http://api.myapp.com/courses/';
        return this.http.get(url);
    }
}
Enter fullscreen mode Exit fullscreen mode

There are, however, several issues with this approach:

  • Bug-prone: There is a wide range of potential bugs that won't be caught early on because we don't make use of TypeScript's static typing.
  • High coupling: any change to the API's data schema will bubble up through the whole code base, even when it doesn't result in functional changes (e.g. renaming fields).

As a result, while the service itself is easy to maintain and easy to read, it definitely won't be the same for the rest of our code base.

The need to adapt

So if simply using the JSON object is not sufficient, what better options do we have?

Well, let's think about the two issues above.

First, the code was bug-prone because we didn't make use of TypeScript's static typing and OOP features (such as classes and interfaces) to model the data. One way to fix this would be to create instances of a specific class (the model) so that TypeScript can help us work with it.

Second, we encountered high coupling with respect to the API data format. This is because we didn't abstract this data format from the rest of our application components. To solve this issue, one way could be to create an internal data format and map to this one when processing the API response.

This is it: we have just conceptualized the Model-Adapter pattern. It comprises of two elements:

  • The model class keeps us within the realm of TypeScript, which has indeed tons of advantages.
  • The adapter acts as the single interface to ingest the API's data and build instances of the model, which we can use with confidence throughout our app.

I find diagrams always help to grasp concepts, so here's one for you:

The Model-Adapter pattern, visualised.

Model-Adapter by example

Now that we've seen what the model-adapter pattern is, how about we implement it in our course system? Let's start with the model.

The Course model

This is going to be a simple TypeScript class, nothing too fancy:

// app/core/course.model.ts
export class Course {
  constructor(
    public id: number,
    public code: string,
    public name: string,
    public created: Date,
  ) { }
}
Enter fullscreen mode Exit fullscreen mode

Note how created is a Date Javascript object, even though the API endpoint gives it to us as a string (ISO-formatted date). You can already see the adapter lurking around at this point.

Now that we have the model, let's start writing a service where the actual HTTP requests will be made. We'll see what we come up with.

Stubbing out the CourseService

Because we're going to make HTTP calls, let's first import the HttpClientModule in our AppModule:

  // app/app.module.ts
  import { BrowserModule } from '@angular/platform-browser';
  import { NgModule } from '@angular/core';
+ import { HttpClientModule } from '@angular/common/http';

  import { AppComponent } from './app.component';

  @NgModule({
    declarations: [
      AppComponent
    ],
    imports: [
      BrowserModule,
+     HttpClientModule,
    ],
    providers: [],
    bootstrap: [AppComponent]
  })
  export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

We can now create the CourseService. As per our design, we'll define a list() method supposed to return the list of courses obtained from the GET: /courses/ endpoint.

// app/core/course.service.ts
import { Injectable } from '@angular/core';
import { Course } from './course.model';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class CourseService {

  list(): Observable<Course[]> {
    // TODO
    return of([]);
  }
}
Enter fullscreen mode Exit fullscreen mode

As we already mentioned, a good thing about Angular's HttpClientModule is that it has built-in support for JSON responses. Indeed, the HttpClient will by default parse the JSON body and build a JavaScript object out of it. So let's try and use that here:

// app/core/course.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Course } from './course.model';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class CourseService {
  private apiUrl = 'http://api.myapp.com/courses';

  constructor(private http: HttpClient) { }

  list(): Observable<Course[]> {
   return this.http.get(this.apiUrl);
  }
}
Enter fullscreen mode Exit fullscreen mode

At this point, TypeScript's compiler will unfortunately rant out (which is a good point!):

TypeScript is not happy… Why?

Of course! We haven't built Course instances from the raw data we retrieved. Instead, we're still returning an Observable<Object>. We can fix this using RxJS's map operator. We'll map the data array to an array of Course objects:

// app/core/course.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Course } from './course.model';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class CourseService {

  private apiUrl = 'http://api.myapp.com/courses';

  constructor(private http: HttpClient) { }

  list(): Observable<Course[]> {
    return this.http.get(this.apiUrl).pipe(
      map((data: any[]) => data.map((item: any) => new Course(
        item.id,
        item.code,
        item.name,
        new Date(item.created),
      ))),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Phew! This will work fine and return an Observable<Course[]> as expected.

However, this code has a couple of issues:

  • It is hard to read as the adaptation code clutters the service.
  • It is not DRY (Don't Repeat Yourself): we'd need to reimplement this logic in all other methods that need to build courses from an API item.
  • It puts high cognitive load on the developer: we need to refer to the course.model.ts file to make sure we provide the correct arguments to new Course().

So, there must be a better way

Enter: the adapter

And of course, there is! 🎉

This is when the adapter comes in. If you're familiar with Gang of Four's design patterns, you might recognize the need for a Bridge here:

Decouple an abstraction from its implementation so that the two can vary independently.

This is exactly what we need (but we'll call it an adapter instead).

The adapter essentially converts the API's representation of an object to our internal representation of it.

In fact, we can even define a generic interface for adapters like so:

// app/core/adapter.ts
export interface Adapter<T> {
    adapt(item: any): T;
}
Enter fullscreen mode Exit fullscreen mode

So let's build the CourseAdapter. Its adapt() method will take a single course item (as returned by the API) and build a Course model instance out of it. Where should you place this? I would recommend inside the model file itself:

// app/core/course.model.ts
import { Injectable } from '@angular/core';
import { Adapter } from './adapter';

export class Course {
    // ...
}

@Injectable({
    providedIn: 'root'
})
export class CourseAdapter implements Adapter<Course> {

  adapt(item: any): Course {
    return new Course(
      item.id,
      item.code,
      item.name,
      new Date(item.created),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that the adapter is an injectable. It means we'll be able to use Angular's dependency injection system, as for any other service: add it to the constructor, and use it right away.

Refactoring the CourseService

Now that we've abstracted most of the logic into the CourseAdapter, here's how the CourseService looks like:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Course, CourseAdapter } from './course.model';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class CourseService {

  private apiUrl = 'http://api.myapp.com/courses';

  constructor(
    private http: HttpClient,
    private adapter: CourseAdapter,
  ) { }

  list(): Observable<Course[]> {
    return this.http.get(this.apiUrl).pipe(
      // Adapt each item in the raw data array
      map((data: any[]) => data.map(item => this.adapter.adapt(item))),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that, now:

  • The code is more DRY: we have abstracted the logic in a separate element (the adapter) which we can reuse at will.
  • The CourseService is more readable as a result.
  • Cognitive load is reduced because the model and its adaptation logic are kept in the same file.

I also promised to you that the Model-Adapter pattern would help reduce coupling between the API and our app. Let's see what happens if we encounter a…

Data format change!

The guys from the API team have made changes to the API's data format. Here's how a course item now looks like:

{
    "id": 1,
    "code": "adv-maths",
    "label": "Advanced Mathematics",
    "created": "2018-08-14T12:09:45"
}
Enter fullscreen mode Exit fullscreen mode

They changed the name of the name field into label!

Previously, everywhere our frontend app used the name field, we would have had to change it to label.

But we're now safe! Since we have an adapter whose role is precisely to map between the API representation and our internal representation, we can simply change how the internal name is obtained:

@Injectable({
  providedIn: 'root'
})
export class CourseAdapter implements Adapter<Course> {

  adapt(item: any): Course {
    return new Course(
      item.id,
      item.code,
-     item.name,
+     item.label,
      new Date(item.created),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

A one-line change! And the rest of our app can continue to use the name field as usual. Brilliant. ✨

Good principles always apply

I have been using the Model-Adapter pattern in most of my Angular projects that involve REST API interaction, with great success.

It has helped me reduce coupling and make full use of the power of TypeScript.

All in all, it really boils down to sticking to good ol' principles of software engineering. The major one here is the single responsibility principleevery code element should do one thing, and do it well.

If you're interested, you can find the final code on GitHub: ng-courses.

I hope this simple architectural tip will help you improve the way you integrate APIs in Angular apps. However, if you've been successful yourself with another approach, I'd love to hear about it!


Update (March 1, 2019): I wrote a follow-up post for this article. It explains how to use the CourseService created here in an Angular component:

Stay in touch!

If you enjoyed this post, you can find me on Twitter for updates, announcements and news. 🐤

Discussion (34)

Collapse
amineamami profile image
amineamami • Edited

The http methods take an optional generic type so get<Course>(url) , this renders your adapter useless :

list(): Observable<Course[]> {
const url = "${this.baseUrl}/";
return this.http.get<Course>(url);
}

Collapse
florimondmanca profile image
Florimond Manca Author

Interesting point. I never thought that the generic type you could pass to .get(), .post(), etc, could be used by Angular to perform the conversion. Do you have examples of that being used?

The case for adapters is also handling complex conversions, such as building nested objects and the like. For example, what if GET: /courses/ returned course objects with a students field, representing a list of Student objects? Can generic type conversion handle that?

Also, how can generic type conversion decide to build a Date object out of the created field (a string)?

Happy to discuss further. :)

Collapse
amineamami profile image
amineamami • Edited

That code snippet works like a charm i’m using it currently it been like that since angular 5 or 4 not sure, your right about the date thingy but if you send it from backend as a date type. You’ll get it as a date type too in angular.

Thread Thread
florimondmanca profile image
Florimond Manca Author

Cool! Glad to hear simple type conversions work.

To my knowledge there is no Date type in JSON, though, so that still means the model-adapter pattern is needed in more complex use cases.

Thread Thread
amineamami profile image
amineamami

In typescript/JavaScript there is obviously, you’ll access it like a normal property Obj.dateProperty

Collapse
avronyc profile image
4lv4r0 • Edited

the generic type you are able to pass to an http get or post, etc... its merely to allow typed responses. The adapter is still needed if your model objects contain any methods, etc. So for instance if you have a method such a getFullName() within your User model, this wont be available for the objects returned by http.get. its still necessary to do a map in order to actually create the User objects based on the data received. Hope this clarifies a bit

angular.io/guide/http#requesting-a...

Collapse
buinauskas profile image
Evaldas

The company I work for, we use a static method to construct that instead of a service:

export class Course {
  constructor(
    public id: number,
    public code: string,
    public name: string,
    public created: Date,
  ) { }

  static adapt(item: any): Course {
    return new Course(
      item.id,
      item.code,
      item.name,
      new Date(item.created),
    );
  }
}

And then we can use it as follows:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Course } from './course.model';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class CourseService {
  private apiUrl = 'http://api.myapp.com/courses';

  constructor(private readonly http: HttpClient) {}

  list(): Observable<Course[]> {
    return this.http.get<any[]>(this.apiUrl).pipe(
      // Adapt each item in the raw data array
      map(data => data.map(Course.adapt))
    );
  }
}

Do you see any of these approaches being better than another for some reason? They produce exactly the same result, but yours utilizes Angular's DI, mine does not.

Collapse
florimondmanca profile image
Florimond Manca Author

It's a very clever alternative implementation of this pattern. I like how this approach binds the adaptation code to the model class. Also saves an import statement, and the DI boilerplate. :-) I'd definitely try this out in my next Angular projects!

Collapse
noelsebastian22 profile image
noelsebastian22 • Edited

Hi Florimond Manca,

I am using the above mentioned method in my current project. Now I'm stuck in one requirement where the value of one variable should change according to the some conditions .

example :-

export class Course {
constructor(
public id: number,
public code: string,
public name: string,
public created: Date,
) { }

static adapt(item: any): Course {
return new Course(
item.id,
item.code,
item.name,//Change the name to "john doe" if item.id=0 and item.code = john
new Date(item.created),
);
}
}

Is there any option for me to add a function which would perform this action?

Collapse
buinauskas profile image
Evaldas

Indeed. Map statement reads really nicely. 👌

Collapse
marcel_cremer profile image
Marcel Cremer

Interesting approach to map the values to a typescript object. I currently use something like

class Person {
 firstname: string;
 lastname: string;
 age: number = 21;

 constructor(initialValues: Partial<Person>) {
  Object.assign(this, initialValues);
 }
}
...
 list(): Observable<Person[]> {
    const url = `${this.baseUrl}/`;
    return this.http.get(url).pipe(
      map((data: any[]) => data.map(item => new Person(item)),
    );
  }
...

to return the correct type and also be able to set default values in the class, but this doesn't decouple the code as nice as your Adapter. Might be worth a try, thank you :)

Collapse
florimondmanca profile image
Florimond Manca Author

Thanks for sharing your approach :) I also used it in some cases when I was sure the fields wouldn't change much; but as soon as they did, I had to resort to an adapter.

(Also, I've just learnt about TypeScript Partial, thank you ✌️)

Collapse
gabrieltaets profile image
Gabriel Taets

Just be careful with Object.assign() in that constructor. While you're telling Typescript that you'll be receiving a Partial as an argument, that is not validated in runtime. So you might have a Person object with more (or less) properties than expected, should your back-end misbehave for some reason.

Collapse
thiltal profile image
Zdeněk Mlčoch

For advanced front-end developers there is a way of generate the adapter part from openApi (swagger) specification. Even a beginner can generate angular project on page editor.swagger.io/. It can take part in developer flow so you are warned by typescript compiler if the api has changed.

Collapse
crywolfe profile image
Gerry Wolfe

This is similar to what we do at my place of employment.
We build the back-end service in Java, using Spring and Spring/Swagger annotations.
We take the generated front-end controllers and models from Swagger and then, as an example, for a 'get' request, send the return json object to an NGXS state class that we build out and use the model in the state class to set the respective states.

Collapse
stereobooster profile image
stereobooster

Also transform.now.sh/json-to-io-ts/ or the big list of similar tools here

Collapse
crisz profile image
crisz

I have a question, when we change the name of the field from "name" to "label", wouldn't be better if we refactor our front-end code? I mean, it's better if there's a corrispondence between front-end model and back-end model, and if for example I want to update a course sending a POST request, should I provide another adapter to reflect the changes?

Collapse
florimondmanca profile image
Florimond Manca Author

Refactoring is an on-going process, and this kind of slight difference between the backend and the frontend is certainly worth fixing at some point. The key point here is that you don't have to refactor right now — contrary to not using an adapter, where all your code base might break. You can just make that one-line change, the code will work and the frontend team can refactor later. 👍 That's decoupling in every sense of the word.

As for the POST request, very good point! What to do when you need to adapt data "the other way around"? Well, I think it's the same idea. You can implement the reverse operation: Model instance -> API data. I generally do it on another method on the adapter, like .encode() or .serialize(). This way, we keep the adapter as a single interface between the external and internal representation (now in both directions).

Collapse
crisz profile image
crisz

Thank you for the reply!

Collapse
enjoiful profile image
Dan Strengier

Great article, thanks!

Can you help me understand the pros and cons of the following approach, compared to the adapter approach?

In the following code (which I did not write), you can see the author is mapping an unknown object to the Hero class. If the API changes, they can simply change the constructor mapping, and keep the Hero properties as is.

Why not just put all of that adapter logic in the model's constructor?

export class Hero  {
  id: string;
  name: string;
  alterEgo: string;
  likes: number;
  default: boolean;
  avatarUrl: string;
  avatarBlurredUrl: string;
  avatarThumbnailUrl: string;

  constructor(hero: any = {}) {
    this.id = hero.id;
    this.name = hero.name || '';
    this.alterEgo = hero.alterEgo || '';
    this.likes = hero.likes || 0;
    this.default = hero.default || false;
    this.avatarUrl = hero.avatarUrl || '';
    this.avatarBlurredUrl = hero.avatarBlurredUrl || '';
    this.avatarThumbnailUrl = hero.avatarThumbnailUrl || '';
  }
}
Collapse
milankovach profile image
Milan Kovac

Hi Florimond,
this is great but can you add short description (maybe here in comment section) how to use your services in other components for the n00bs? Thanks! :)

Collapse
florimondmanca profile image
Florimond Manca Author

Hey! You're right, I suppose it would be very useful to see how the CourseService can be used in practice to fetch and display data in a component. I'm actually writing a follow-up post for that! Will link to it here once it's published. :-)

Collapse
florimondmanca profile image
Florimond Manca Author

Will cross-publish here soon, but I've just published it to my blog: Consuming APIs In Angular: Displaying Data In Components. 🙌

Thread Thread
milankovach profile image
Milan Kovac

Wow! Thanks! That was really fast! :)

Collapse
stereobooster profile image
stereobooster

As far as I understood adapter doesn't really validate types, it only checks the presence of fields in the structure. You can validate actual input with something like io-ts or sarcastic. I wrote a small article about IO validation if you interested.

Collapse
florimondmanca profile image
Florimond Manca Author

Yep, this pattern won’t validate the JSON data you receive. In fact, unless you implement error handling it will probably result in errors if the data doesn’t fit the adapter (eg providing a value that cannot be converted to a Date).
It’s simply that this pattern is only applicable if you know exactly the schema of the data you’re going to receive (that is, you built the API or have thorough documentation about its data format).
So good point for mentioning this limitation and providing resources to implement actual validation. :)

Collapse
seagerjs profile image
Scott Seager

The possibilities of Angular built-ins performing similar work aside, this was a great example of the practical application of sound design patterns and SOLID principles. Nice job!

Collapse
florimondmanca profile image
Florimond Manca Author

Thanks! Really appreciated. Do you know of any Angular built-in doing this kind of thing — aside from the generic type doing automatic conversions, which was mentioned in another thread?

Collapse
seagerjs profile image
Scott Seager

No ... I was just referring to he conversation earlier in this thread. Framework features are great and should be leveraged when available/known, but sound design practices are always applicable.

Collapse
rierjarv profile image
Riku Järvinen

Good work, I'll be doing some refactoring with these ideas in mind. Thanks!

Collapse
ah profile image
Adrian H

Congrats! You are getting very close the the Ember Data way of doing this:

guides.emberjs.com/release/models/

Collapse
chiangs profile image
Stephen E. Chiang

This is really great, I have been worrying something similar, but hadn't thought to make it a reusable interface for models.

Collapse
hathawayjess profile image
Jess Hathaway

Thank you for this! Very well written and easy to understand.

Collapse
andremantas profile image
André Mantas • Edited

Imo, CourseService should receive an Adapter<Course> and not the concrete CourseAdapter.

Forem Open with the Forem app