DEV Community

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

Consuming APIs in Angular – The Model-Adapter Pattern

Florimond Manca on September 05, 2018

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 mo...
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