Consuming APIs in Angular – The Model-Adapter Pattern - A TypeScript-friendly pattern to improve how you integrate Angular apps and REST APIs.
Today, I want to talk about architecture and share with you a design patternthat 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!
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?
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/
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"
}
]
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);
}
}
There are, however, several issues with this approach:
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.
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:
I find diagrams always help to grasp concepts, so here’s one for you:
The Model-Adapter pattern, visualised.
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.
Course
modelThis 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,
) { }
}
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.
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 { }
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([]);
}
}
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);
}
}
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),
))),
);
}
}
Phew! This will work fine and return an Observable<Course[]>
as expected.
However, this code has a couple of issues:
course.model.ts
file to make sure we provide the correct arguments to new Course()
.So, there must be a better way…
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;
}
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),
);
}
}
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.
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))),
);
}
}
Note that, now:
CourseService
is more readable as a result.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…
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"
}
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),
);
}
}
A one-line change! And the rest of our app can continue to use the name
field as usual. Brilliant. ✨
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 principle — every 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!
#angular #web-development #api