Proof Of Concept: Using Axios As Your HTTP Client In Angular 6.0.0
The other day, I was listening to Wes Bos and Scott Tolinski talk about learning new technologies on the Syntax FM podcast. In that discussion, Wes mentioned that one aspect that's great about working with an unopinionated framework is that you can bring your own favorite libraries with you as you move from project to project. In particular, he mentioned how much he loves the Axios HTTP library. I've never used Axios myself. But, I do work with Angular, which is a fairly unopinionated JavaScript framework. As such, I wanted to throw together a quick little proof-of-concept showcasing how easy it would be to start using Axios as your HTTP client library of choice in Angular 6.0.0.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
To be clear, this is not a tutorial on how to use Axios. This post is literally the first time I've ever used it; so, I'm certainly not one who should be talking about its features and caveats. This demo is strictly a proof-of-concept on using Axios - or any HTTP library, really - in an Angular application.
One beautiful part of Angular is that it uses Zones (via Zone.js). And, when an Angular application bootstraps, it drops you into the Angular Zone (NgZone) by default. This means that you can start using any asynchronous control flow (such as an XHR library) and the Angular application will implicitly know about it. Which, in turn, allows Angular to seamlessly weave said asynchronous actions into the component tree's change detection algorithm.
Ultimately, what this means is that you can start using the Axios HTTP library and it will "just work."
To see this proof-of-concept in action, let's create an Angular service - ApiClient - that wraps the Axios library. This approach isn't Axios-specific; it's just something I like to do so that I can create an injectable HTTP transport that encapsulates the intricacies of dealing with a particular remote API (Application Programming Interface). I would do the same thing even if I was using Angular's core HTTPClient.
To keep things super simple - it's a proof-of-concept after all - our ApiClient will only implement one method, GET:
import axios from "axios";
import { AxiosInstance } from "axios";
import { ErrorHandler } from "@angular/core";
import { Injectable } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
export interface Params {
[ key: string ]: any;
}
export interface GetOptions {
url: string;
params?: Params;
}
export interface ErrorResponse {
id: string;
code: string;
message: string;
}
@Injectable({
providedIn: "root"
})
export class ApiClient {
private axiosClient: AxiosInstance;
private errorHandler: ErrorHandler;
// I initialize the ApiClient.
constructor( errorHandler: ErrorHandler ) {
this.errorHandler = errorHandler;
// The ApiClient wraps calls to the underlying Axios client.
this.axiosClient = axios.create({
timeout: 3000,
headers: {
"X-Initialized-At": Date.now().toString()
}
});
}
// ---
// PUBLIC METHODS.
// ---
// I perform a GET request with the given options.
public async get<T>( options: GetOptions ) : Promise<T> {
try {
var axiosResponse = await this.axiosClient.request<T>({
method: "get",
url: options.url,
params: options.params
});
return( axiosResponse.data );
} catch ( error ) {
return( Promise.reject( this.normalizeError( error ) ) );
}
}
// ---
// PRIVATE METHODS.
// ---
// Errors can occur for a variety of reasons. I normalize the error response so that
// the calling context can assume a standard error structure.
private normalizeError( error: any ) : ErrorResponse {
this.errorHandler.handleError( error );
// NOTE: Since I'm not really dealing with a production API, this doesn't really
// normalize anything (ie, this is not the focus of this demo).
return({
id: "-1",
code: "UnknownError",
message: "An unexpected error occurred."
});
}
}
Other than some of the TypeScript notations, there's not really anything particularly special about this Class. As you can see, it merely creates an instance of the Axios client with some API-level defaults. Then, it proxies calls to the Axios client, intercepting the request and response payloads.
And now that we have this ApiClient service, we can inject it into other services and components. In this proof-of-concept, I'm going to inject it directly into the AppComponent, where we'll use it to load a list of friends:
// Import the core angular services.
import { Component } from "@angular/core";
// Import the application components and services.
import { ApiClient } from "./api-client";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
interface Friend {
id: number;
name: string;
}
@Component({
selector: "my-app",
styleUrls: [ "./app.component.less" ],
template:
`
<p>
<a (click)="loadFriends()">Load Friends</a>.
</p>
<ng-template [ngIf]="friends.length">
<h2>
You have {{ friends.length }} friends!
</h2>
<ul>
<li *ngFor="let friend of friends">
{{ friend.name }} ( id: {{ friend.id }} )
</li>
</ul>
</ng-template>
`
})
export class AppComponent {
public friends: Friend[];
private apiClient: ApiClient;
// I initialize the app-component.
constructor( apiClient: ApiClient ) {
this.apiClient = apiClient;
this.friends = [];
// In order to demonstrate that Axios will engage the XSRF protection, let's
// set an XSRF-TOKEN cookie.
// --
// NOTE: This would normally be some unpredictable value set by the server.
document.cookie = "XSRF-TOKEN=server-generated-token";
}
// ---
// PUBLIC METHODS.
// ---
// I load the list of friends.
public async loadFriends() : Promise<void> {
try {
// NOTE: For the sake of simplicity, I'm letting the Component talk directly
// to the ApiClient. However, in a production app, I'd create an abstraction
// around Friend access (ie, something like FriendService or FriendGateway)
// which would handle the low-level details of the ApiClient request and
// error handling. But, since this is just a post about using Axios in
// Angular, I'm removing the middle man for the controlled scenario.
this.friends = await this.apiClient.get<Friend[]>({
url: "api/friends.json",
params: {
limit: 10
}
});
} catch ( error ) {
console.error( error );
}
}
}
Normally, I would wrap the Friend data access inside some sort of intermediary Service provider (ex, FriendService) that knows more about the Friend data landscape. However, in order to keep things simple, I'm allowing the Component tree to grab the ApiClient directly - Angular is fairly unopinionated, so you can pretty much wire things together however you see fit.
And, once the AppComponent has the ApiClient instance, clicking on the template link will trigger an HTTP request that will eventually cause the view to be updated:
As you can see, everything "just worked." Axios made the AJAX (Asynchronous JavaScript and JSON) call to the server. Then, Angular seamlessly integrated the Axios response into the AppComponent's view template. Angular was able to do this because our Axios client was operating in the Angular Zone. This means that when the XHR events completed, Angular knew that it was possible something in the application's View Model could have been changed. As such, it executed its change-detection algorithm and updated the AppComponent's view template to reflect the populated collection of friends.
As an added bonus, Axios happens to use the same XSRF (Cross-Site Request Forgery) token approach that Angular uses. Since we defined an XSRF-TOKEN cookie in the AppComponent (which would normally be defined by the Server), Axios automatically injected the "X-XSRF-TOKEN" header in the outgoing HTTP request.
ASIDE: At the time of this writing, Angular's HTTPClient will only inject the "X-XSRF-TOKEN" into mutating requests like PUT, POST, and DELETE. No matter how they rationalize it, this seems like a bug; and, is at the very least, a break from their historical treatment of XSRF tokens.
While Angular does use dependency-injection (DI) and has embraced the use of TypeScript, the Angular framework itself is fairly unopinionated. If you want to use their HTTPClient module, go for it! But, if you'd rather use your own favorite HTTP client library - something like Axios - that's also fine. Thanks to Angular's use of Zone.js, asynchronous control flows seamlessly integrate with Angular's change-detection mechanism.
Want to use code from this post? Check out the license.
Reader Comments
Hey, very helpfull tutorial, but how the implementation of a post method should look like? Thank you!