Angular and RxJS Development Patterns for Fetching Data From APIs

The most common RxJS operators and techniques for fetching data from APIs

Radek Busa
Level Up Coding

--

Photo by Manuela Adler from Pexels

As beginners in Angular programming, many developers, even with theoretical knowledge of RxJS, have trouble when fetching data from APIs. The situations they try to handle are usually similar though. This article will list the most common situations you may stumble upon when developing Angular applications and present battle-proven solutions to them.

🚷 A Few Antipatterns to Begin With

In order to avoid code smells and architectural problems in Angular apps with API data fetching, let's walk through the most common antipatterns. The patterns in the later chapters of this story will address those issues.

Nested Subscribing

One rather naïve solution to such problem is to use something like this, usually inside ngOnInit or another component lifecycle hook:

this.articleService.getById(articleId).subscribe(article => {
this.userService.getById(article.authorId).subscribe(user => {
article.author = user;
this.article = article;
});
});

You may say “That's acceptable”. But do not forget that some objects originating from many APIs can have many levels of nested objects. Plus, nobody can predict (especially on big projects) how your API will look a year from now. The code for handling such extreme cases would then become hard to read:

this.articleService.get(...).subscribe(article => {
this.userService.get(...).subscribe(user => {
this.addressService.get(...).subscribe(address => {
this.countryService.get(...).subscribe(country => {
// ufff. now assign the stuff somewhere
});
});
});
});

The message hidden in all this is: Nested subscribes are just disguised callback hell.

Data Layer Responsibility Creep

Data layer responsibilities creeping into the presentation layer is an antipattern seen very often in numerous projects. Let's look at an example of such antipattern:

@Component()
export class MyComponent implements OnInit {
... public ngOnInit(): void {
this.articleService.getById(articleId).subscribe(article => {
this.userService.getById(article.authorId).subscribe(user => {
article.author = user;
this.article = article;
});
});
}
}

Why could this be an antipattern? Imagine a scenario where you want to reuse existing APIs for a new Angular application with a different look and different sets of screens — a mobile application for instance. Having such code inside your components would almost certainly mean either copypasting and refactoring or if the code gets forgotten, a ground-up reimplementation. Both needlessly expensive and boring.

However, with such operations present in your services, the reusability of the code could become much higher:

@Injectable()
export class ArticleService {
...
public constructor(protected userService: UserService) {} protected getById(artId: Number): Observable<GetArticleResponse> {
...
}
public getArticle(articleId: Number): Observable<Article> {
// combine the data from userService and articleService
// and return an Observable of combined data
}
}

Usage of such operation becomes much more compact in its consuming component:

@Component()
export class MyComponent implements OnInit {
... public ngOnInit(): void {
this.articleService.getArticle(articleId).subscribe(article => {
this.article = article;

});
}
}

So remember — the best way to have the most reusable code in your Angular project is to encapsulate as most of your data transformations to service layer operations as possible.

🧩 Loading Additional Data

Also known as Data enrichment. This one is quite common, especially in big projects with environments utilizing SOA (Service-Oriented Architecture) and microservices. In such scenarios, there is a need to fetch big objects or a collection of objects with data originating from multiple APIs.

For the sake of the following examples, let's assume we are building a minimalistic blog app with a backend consisting of two microservices with REST APIs — one manages the articles and another one manages the users. Each article contains who wrote it based on the User ID, an identifier originating from the User microservice.

The diagrams below depict data sources (blue), their response data (white) and the data we want the Angular service to produce our component (yellow).

Loading Additional Data for One Object

Loading Additional Data For One Object — Scenario Illustrated

Let's display the first and last name of the article author on the article detail screen by joining the outer object (Article) with the inner object (User).

The solution to this is simple:

  • When the article arrives from the server to the browser, switchMap will switch the observable loading the article (aka. inner observable) to an observable which will load the author (aka. the outer observable).
  • After the author arrives, the map operator will change the article object as needed and return the article enriched by the user data. Returning the article and not the user is very important because the source of result data is now the inner observable.

Loading Additional Data for Multiple Objects

Loading Additional Data For Multiple Objects — Scenario Illustrated

Let's show the first and last names of article authors on a screen containing a list of available articles. That will incorporate kicking off a request for each distinct authorId in the array of articles.

This one might not be so clear, let's break it down:

  • switchMap will take the array with articles and using the from observable factory function will make a stream from it.
  • The second operator will do the exact same inner logic as the previous example, however, it is not switchMap but concatMap. Note that RxJS also provides an operator named mergeMap which will do the same job as concatMap but might not respect the article order.
  • The last operator toArray will just take the stream and join it back to an array after the observable of array items completes.

Loading Additional Data for Multiple Objects Using Bulk Operation

Loading Additional Data For Multiple Objects Using Bulk Operation — Scenario Illustrated

From an API communication standpoint, this query pattern is better in terms of performance — using bulk operations is generally network-friendlier because each request has its overhead — i.e. the network must carry additional data (HTTP headers, TCP protocol data, IP protocol data, etc.) for each individual HTTP request. That means the fewer requests you make, the faster your application will be on slow networks per the same quantity of response data.

As seen in the snippet above, the operators used are the same as in the first example except the switchMap operator aggregates an array of author IDs for subsequent query and the map operator finds the right author for each article using JavaScript array searching methods.

🛣 Handling Parallel Requests

As opposed to the previous case, this approach is suitable in situations when data to be loaded are independent on each other, so you can fire all the requests at once.

Parallel Requests Without Waiting

Just fire them and that's it:

this.articleService.getById(articleId).subscribe(...);
this.relatedArticleService.getByArticleId(articleId).subscribe(...);
this.userNotificationService.getAll().subscribe(...);

Parallel Requests With Waiting

In case you need to wait for a group of requests fired in parallel, the forkJoin observable factory function is here to help. Once subscribed to, it will create an observable that will in its subscriber allow performing consequent actions once all the requests specified in forkJoin return a response:

forkJoin({
article: this.articleService.getById(articleId),
related: this.relatedService.getByArticleId(articleId),
notifications: this.userNotificationService.getAll()
}).subscribe(values => {
this.article = values.article;
this.relatedArticles = values.related;
this.notifications = values.notifications;
// do what you want
});

⏱ Request Timeouts

This pattern is useful especially in environments where user experience matters. And is very simple indeed:

this.articleService.getById(articleId).pipe(
timeout(15000), // timeout in msecs
...
).subscribe(...);

The timeout operator will emit an error once no response comes within the specified time, so implementing an error handler using either the catchError operator (for creating a fail-safe response indicating a failure or no data — empty response for instance) or using the error observer callback function (for handling the situation in the component such as displaying a refresh button) is a must.

Assuming that the application will most probably need to utilize uniform timeout length for every network request, putting this operator into a global HTTP interceptor might be a good idea:

@Injectable()export class TimeoutInterceptor implements HttpInterceptor {
public intercept(
req: HttpRequest<any>,
next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
timeout(15000)
);
}
}

📜 Conclusion

This story presented 6 different patterns for solving problems you may come into while developing an HTTP service in Angular or pure RxJS.

Have you encountered a different recurring problem related to API data fetching and solved it in an interesting way? Post your experience to the comment section below!

--

--

I’m a senior full-stack web developer based in Prague, currently studying prg.ai, an AI/ML enthusiast course. www.radekbusa.eu www.linkedin.com/in/radekbusa