I know at first this will sound like an old topic but believe me you will definitely find some new features within this article.
What to expect from the article?
- Starting from scratch calling Web API (making GET request)
- Displaying full results with Headers, Code, Status, Status Text, etc.
- Dealing with errors (Server-side Errors and Client-side Errors)
- Some RxJS operators (like retry, catch errors, map etc.)
- Search using debouncing
So, let’s dive into HTTP Client from scratch.
First, in order to use HTTP Client in an Angular application, we need to import it in the app.module.ts file, as shown below.
- 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 { }
Generate Service and provide it in root
So, as a first step in fetching the data from our Web API, we will create a service. And as you all might know, services are DRY!! (Don’t Repeat Yourself).
Use the following command for generating services using Angular CLI.
ng g s task
Then, in order to use a service in our application, we first need to provide the services. In general cases, we will be providing our service in our root. From Angular version 6.x and above, we don’t need to provide our services manually to app.module.ts. Instead, we will be following the decorator in our newly created service.
- @Injectable({
- providedIn: 'root'
- })
So, we can inject the service anywhere in our application.
Introduction to Web API
Now, before making a call to my Web API, I would like to give a brief introduction of the Web API and what it will return.
URL - https://rkdemotask.herokuapp.com/tasks/
GET: Fetch all tasks and return Id, Title, and Status
Below is the sample data.
Note
This Web API is publically available, so there may be chances of garbage data or no data, but if there is data, it will look like the following.
Generate Task Class
- export class Task {
- public constructor(
- public Id:string,
- public Title:string,
- public Status:string){
- }
- }
We have created a class to provide the data structure and to store the data which is returned by the Web API.
Note
Remember to give the same name to the properties to avoid any error in displaying the data on the component.
Creating Method In Service
- import { Task } from './task';
- import { Injectable } from '@angular/core';
- import { HttpClient } from '@angular/common/http';
-
- @Injectable({
- providedIn: 'root'
- })
- export class TaskService {
- url:string='https://rkdemotask.herokuapp.com/tasks/';
- constructor(public _http:HttpClient) { }
- getAllTask(){
- return this._http.get<Task>(this.url);
- }
- getAllTaskWithFullResponse(){
- return this._http.get(this.url,{observe:'response'});
- }
- }
First, I have created the instance _http of HttpClient in the constructor.
Then, we are having one method named getAllTasks which will simply make HTTP calls to my Web API and also return all the tasks.
The second method, getAllTaskWithFullResponse, is almost the same method but it will return the full response, i.e., it will return us the response with headers, code, Status Text, Body etc. whereas getAllTask will only return us the data. We can make the full response true by simply passing an observe flag with the response.
Let me show you the result as a snapshot but before that, let me first inject the service on our component and call both the methods.
Injecting Service to app.component.ts
- import { TaskService } from './task.service';
- import { Component,OnInit } from '@angular/core';
- import { Task } from './task';
-
- @Component({
- selector: 'app-root',
- templateUrl: './app.component.html',
- styleUrls: ['./app.component.css']
- })
- export class AppComponent implements OnInit {
- title = 'httpclientDemo';
- constructor(private _taskdata:TaskService){}
- ngOnInit(){
- this._taskdata.getAllTask().subscribe(
- (data)=>{
- console.log(“only data” +data);
- }
- );
- this._taskdata.getAllTaskWithFullResponse().subscribe(
- (data:any)=>{
- console.log(“full response”+data);
- }
- );
- }
- }
Note
Always use "Subscribe" because the httpClient method will not begin its HTTP request call until you subscribe() with the method.
Full Response with headers,body,status text and etc. (getAllTaskWithFullResponse)
Only data (getAllTask)
Until now, it is just a simple HTTP Call with GET request; nothing special in it, right?
Error Handling
So now, let’s dive a little bit deeper with HTTP Client and handle HTTP Errors. Basically, there are two types of errors when you are making an HTTP call.
- Error from server; i.e., server might reject the request with status code 404 or 500. These are due to incorrect addresses or due to internal server errors.
- Client-Side Error, i.e., network error which in turn, does not allow making a call or some error because of RxJS operators.
Both of these errors can be handled by HttpClient under HttpErrorResponse. So now, we will create a method which will deal with the error handling and also print the appropriate message to the user.
Modify the task.service.ts file as shown below.
- import { Task } from './task';
- import { Injectable } from '@angular/core';
- import { HttpClient, HttpErrorResponse } from '@angular/common/http';
- import { throwError, Observable } from 'rxjs';
- import { catchError } from 'rxjs/operators';
- @Injectable({
- providedIn: 'root'
- })
- export class TaskService {
- url:string='https://rkdemotask.herokuapp.com/tasks/';
- constructor(public _http:HttpClient) { }
- getAllTaskWithFullResponse(){
- return this._http.get(this.url,{observe:'response'}).pipe(
- catchError(this.handleError)
- );
- }
- getAllTask(){
- return this._http.get<Task>(this.url);
- }
- private handleError(ex:HttpErrorResponse){
- if(ex.error instanceof ErrorEvent){
- console.log('client side error',ex.message);
- }
- else{
- console.log('server side error',ex.message);
- }
- return throwError('something went wrong');
- }
- }
So first, import HttpErrorResponse @angular/common/http
And after that, create the handleError method in which we will check for type of error. If it is a type of error event, then there will be an error which is from client-side; else the error will be from server-side.
And finally, we are making a call of this method using pipe on our getAllTaskWithFullResponse method and calling catchError which can be imported from ‘rxjs/operators'.
We can use this handleError method with any method from the task.service.ts file.
Using retry()
Sometimes you might have an error due to network interruptions on your mobile phone. These errors simply go away automatically if we try again. The RxJS library will give us a very simple solution to these problems, that is using retry, which can also be imported from ‘rxjs/operators'. In order to use these retry operators, modify your service, as shown below.
- import { Task } from './task';
- import { Injectable } from '@angular/core';
- import { HttpClient, HttpErrorResponse } from '@angular/common/http';
- import { throwError, Observable } from 'rxjs';
- import { catchError, retry } from 'rxjs/operators';
- @Injectable({
- providedIn: 'root'
- })
- export class TaskService {
- url:string='https://rkdemotask.herokuapp.com/tasks/';
- constructor(public _http:HttpClient) { }
- getAllTaskWithFullResponse(){
- return this._http.get(this.url,{observe:'response'}).
- pipe(
- catchError(this.handleError)
- );
- }
- getAllTask(){
- return this._http.get<Task>(this.url).pipe(
- retry(3),
- catchError(this.handleError)
- );
- }
-
- private handleError(ex:HttpErrorResponse){
- if(ex.error instanceof ErrorEvent){
- console.log('client side error',ex.message);
- }
- else{
- console.log('server side error',ex.message);
- }
- return throwError('something went wrong');
- }
- }
So now, to check whether retry is working or not, I will turn off the wi-fi connection and check how many times it will make an HTTP request and after that, it will give us error message.
So, as we can see, it will try for 3 more times before displaying the error message.
Search Using Debouncing
Assume the use case in which we want to create a search application for which we are passing every key stroke to our back end / web API to get the result from the server. Sending each and every keystroke to the back end would be expensive. It’s always a better solution that we can pass parameter to our back end only after the user stops typing. Yes, that’s very easy using RxJs Operators.
We will be using npm package search feature to perform a search operation. Below is the html,
- <input (keyup)="search($event.target.value)" id="name" placeholder="Search"/>
-
- <ul>
- <li *ngFor="let package of packages | async">
- <b>{{package.name}} v.{{package.version}}</b> -
- <i>{{package.description}}</i>
- </li>
- </ul>
This is an ideal html design of search application, which I used on the input box to take input from the user and an unordered list to display its result. As I told you before I am using npm package search services which will return name, version and description, which I am binding to li.
Below is the code snippet for typescript,
- import { Component, OnInit } from '@angular/core';
- import { NpmserachService, NpmPackageInfo } from '../npmserach.service';
- import { Subject, Observable } from 'rxjs';
- import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
-
- @Component({
- selector: 'app-demo',
- templateUrl: './demo.component.html',
- styleUrls: ['./demo.component.css']
- })
- export class DemoComponent implements OnInit {
- public search_subject=new Subject<string>();
- public packages:Observable<NpmPackageInfo[]>;
- constructor(public _data:NpmserachService) { }
-
- search(searchTerm:string){
- this.search_subject.next(searchTerm);
- }
- ngOnInit() {
- this.packages=this.search_subject.pipe(
- debounceTime(1000),
- distinctUntilChanged(),
- switchMap(data=>this._data.search(data))
- );
- }
- }
I had created NpmserachService for making http call and NpmPackageInfo interface to provide data structure.
Here in the above code snippet, I am using Three RxJs Operators:
- debounceTime - It will wait for the user to stop typing (In our case 1 second)
- distinctUntilChanged - It will wait until the search term is changed
- switchMap - Finally, it will send the search term to service to make an HTTP request.
Below is the snippet for NpmsearchService,
- import { Injectable } from '@angular/core';
- import { HttpClient } from '@angular/common/http';
- import { throwError, Observable, of } from 'rxjs';
- import { catchError, retry,map } from 'rxjs/operators';
-
- export interface NpmPackageInfo {
- name: string;
- version: string;
- description: string;
- }
-
- @Injectable({
- providedIn: 'root'
- })
- export class NpmserachService {
- public url:string="https://npmsearch.com/query?q=";
- constructor(public _http:HttpClient) { }
- public search(searchTerm:string):Observable<NpmPackageInfo[]>{
- if (!searchTerm.trim()) { return of([]); }
- return this._http.get(this.url+searchTerm).pipe(
- map((data:any)=>{
- return data.results.map(
- entry=>({
- name:entry.name[0],
- version:entry.version[0],
- description:entry.description[0]
- } as NpmPackageInfo)
- );
- })
- );
- }
- }
Let's check if it is working or not, whether it will make call for each and every key stroke or if it will only make the call to the back-end after the user stops typing.
In the above snapshot, although I typed 3 characters, it had made only one call to the back-end. Now, on the second screen, I will remove the 2 characters but it will make only one call to the back-end.
That’s it! I hope I have made reading worth your time.