Ionic 3, Angular 4.3+ and RxJS Observables: Building an HTTP Service to Communicate with A REST API -- Words (3616)

In this tutorial, we'll see by example how to send HTTP (Ajax) requests to REST API servers (for calling APIs and consuming data) in Ionic 3 and Angular 4.3+.

We'll first start by creating a simple CRUD (Create, Read, Update and Delete) mock server using json-server, then we'll see how to create an Angular service which wraps the new Angular 4.3+ HttpClient module for sending HTTP requests.

We'll also learn how to use the RxJS Observables and the operators such as map() to work with HttpClient requests and responses.

Finally we'll learn with a simple example how to use the HttpClientTestingModule to mock requests with its HttpTestingController service for the purpose of unit testing your Angular 4.3+ or Ionic 3 application without making real API calls.

Introduction

Most modern applications, nowadays, rely on some remote service to consume data. REST APIs provide an interface that allows different clients such as web browsers and mobile devices to communicate with HTTP servers without building a back-end for each client.

Ionic 2+ is based on Angular 2+, a completely rewritten from scratch framework for building web applications with TypeScript (a super-set and strongly typed version of JavaScript created by Microsoft).

This tutorial is a part of a series of tutorials for teaching developers how to create CRUD mobile applications with Ionic 3 and the Angular's HttpClient module.

You'll learn, by a simple example:

  • how to send data to your API server, from an Ionic 3/Angular 4.3+ web or mobile application, by using an HTTP POST request
  • how to retrieve data from your API server by sending HTTP GET requests then use an Ionic List to display these data to users
  • how to update your items by sending HTTP PUT requests with HttpClient
  • How to delete data from a REST server by sending HTTP DELETE requests
  • how to authenticate/authorize Ionic 3 apps with Firebase
  • How to integrate Ionic 3 with a PHP API back-end
  • How to integrate Ionic 3 with a Firebase back-end

Throughout these tutorials we'll be calling different hosted Rest APIs. We will also see how to build APIs using pure PHP and also Django (actually we have already created a tutorial for building a simple API with Django)

We'll be using the new HttpClient module, introduced in Angular 4.3+, instead of the old Angular HTTP service, which is now deprecated in Angular 5.

Understanding REST APIs and RxJS

First, let's cover some HTTP-related terminology:

Clients (like web browsers and mobile devices) communicate with API-based servers through sending HTTP requests. Then servers replay back with HTTP responses. HTTP requests and responses hold metadata and data that get exchanged between the client and the server.

What's a REST API?

An API stands for Application Programing Interface and it provides a way for communication between clients and servers via HTTP methods (POST, GET, PUT and DELETE etc.).

REST is acronym for REpresentational State Transfer. It's an architectural style for distributed systems which is based on 6 constraints among them the client-server architecture and statelessness. You can read more about REST in this Wikipedia article.

What's RxJS (Reactive Programming)?

RxJS is a library for reactive programming using Observables, to make it easier to compose asynchronous or callback-based code. This project is a rewrite of Reactive-Extensions/RxJS with better performance, better modularity, better debuggable call stacks, while staying mostly backwards compatible, with some breaking changes that reduce the API surface. --http://reactivex.io/rxjs/

What you are going to learn?

I'm not going to cover how to create an Ionic 3 application since I have done it in many tutorials before. I also assume you have a development environment setup. For quickly getting up and running you only have to install Node.js, Cordova and the Ionic CLI so make sure you have these requirments installed then next generate a new Ionic 3 project and follow these steps:

  • configure the Ionic app to use the HttpClient module
  • create a mock REST API server using json-server
  • create a service provider to wrap the HttpClient logic
  • use the HttpClientTestingModule to mock HTTP calls when unit-testing your Ionic app

By the end of this article, you will, hopefully, learn:

  • how you can make use of the new Angular 4.3+ HTTP client to send HTTP or Ajax requests from your Ionic 3 mobile application (or also from your Angular 4.3+ web application)
  • how you can use the Angular RxJS Observables
  • how you can mock HTTP requests to use fake endpoints instead of the actual API endpoints when you are unit-testing your Ionic application

So, let’s get started!

Configure the Ionic app to use the HttpClient module

Let's start with the first step--we need to tell the app to use the new Angular 4.3+ module (i.e HttpClient) to send HTTP requests. It's a part of Angular, we just need to import it and then add it to our imports array in src/app/app.module.ts.

So go ahead and open the app module file in src/app/app.module.ts then update it to reflect the changes below:

/* Other imports */
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    MyApp
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    IonicModule.forRoot(MyApp)
  ],
  /* ... */
})
export class AppModule {}

Building a REST API back-end

Actually, we are going to create a fake API back-end using json-server which allows you to quickly generate an API from JSON data to prototype your application before creating the real endpoints. You can then swap the fake back-end with a real API that can be built using your prefered language such as JavaScript and Node.js, PHP or Python etc.

So let's first install json-server. Head back to your terminal or command prompt, navigate to your project's root folder, where package.json exists, then run the following command:

npm install json-server --save-dev

This will install the json-server package and add it to the list of your project's development dependencies

Next, in the same root folder of your project, create a file named db.json then add the following contents to create JSON data for json-server to use when exposing the endpoints:

{
  "products": [
    {
      "id": 1,
      "name": "Product001",
      "cost": 10.0,
      "quantity": 1000,
      "locationId" : 1,
      "familyId" : 1
    }
    ]
}

You can see the full example from this link.

Next you can start the API server with:

json-server --watch db.json 

That's all you need to do! you don't have to setup a database or create API endpoints to start building your front-end application that needs to consume custom endpoints, but of course that's just for testing before you can build your real API back-end.

You now have a REST API server listening on port 3000 which can respond to POST, GET, PUT and DELETE requests.

So just to make sure your back-end is running as expected, you can use your web browser to navigate to http://localhost:3000.

If you just create the one products array like in the previous example, the following endpoints will be exposed:

  • GET /products: get all available products
  • GET /products/:id: get a product with its id
  • POST /products: create a new product
  • PUT /products/:id: update a product by its id
  • DELETE /products/:id: delete a product by its id

If you now use your web browser to navigate to this endpoint: http://localhost:3000/products, you should see a JSON response with all the products we have created in db.json or by sending POST requests (which eventually get persisted in db.json).

Creating an Angular Service/Provider to Encapsulate the Code to Communicate with the REST-API Backend

API calls, using HttpClient module, are asynchronous by nature since you need to wait for the response to come from the remote servers without blocking the app when still waiting.

An HTTP Request will take some time to reach the API server and also the HTTP Response will need time to arrive so this needs to be running in the background before the data can be ready to be consumed.

With Ionic/Angular you can make use of modern JavaScript APIs: Promises and Observables which provide high level abstractions to handle the asynchronous nature of data fetching and API consuming operations or any other operation that takes time to finish.

What's a Promise?

The Promise object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.--MDN

A promise can be:

  • pending: the initial waiting state before eventual fulfilment or rejection.
  • fulfilled: the operation has successfully completed with a value.
  • rejected: the operation has failed with an error.

You place your async actions, either when the promise has successfully resolved or failed, within the .then(()=>{}) and .catch(()=>{}) methods.

Promises can be chained together to handle complex scenarios

Image source

What is an Observable?

Observables are a newer standard than promises which are added, to Angular 2+ (and will be included in the ES7) to allow Angular developers to handle more advanced use cases with clear and concise code. For example you can cancel an observable whenever you need without using external libraries as the case for promises and you can also have multiple return values.

Just like Promises, Observables are abstractions that help you deal with async operations except that they handle asynchronosity in a different way and provide more features so they are becoming preferable over promises among the JavaScript/Angular community.

Unlike promises, which they can only handle single events, observables, on the other hand, can be passed more than one event.

An Observable can be represented as a stream of events that can be handled with the same API and they can be cancelable (this feature is not available for ES6 Promises so you need to use external libraries to do that).

You can use different array-like operators such as map(), forEach(), and reduce() etc. to easily work with observables and handle advanced use cases with a simple and clear API.

The new Angular HttpClient methods return observables objects which can be also converted to promises (using toPromise() operator) so you can use the right abstraction when it's appropriate.

Generating a Service Provider

A service provider is an Angular abstraction which can be used in any other component, page or service via the Angular Dependency Injection or DI. You can use providers to encapsulate code which's common between many places of your application so instead of repeating the same logic in many places you can isolate that code into its own service and inject it wherever you want to use it. This will allow you to comply with the DRY (Don't Repeat Yourself) principle. If you don't know what DRY is, here is its definition from Wikipedia

In software engineering, don't repeat yourself (DRY) is a principle of software development aimed at reducing repetition of software patterns, replacing them with abstractions; and several copies of the same data, using data normalization to avoid redundancy.

By following the DRY principle you place the code which interfaces with your back-end API in one place which makes the app easy maintainable.

Now let's generate our API-interfacing service using the Ionic CLI. Head back to your terminal or command prompt then run the following command to generate a service provider

ionic g provider rest

This command will create a new folder in your project's src/providers, and add your newly created provider to the array of providers in src/app/app.module.ts (If it's not added make sure to do it manually).

/* Other imports */
import { HttpClientModule } from '@angular/common/http';
import { RestProvider } from '../providers/rest/rest';

@NgModule({
  /* ... */
  providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    RestProvider //this is our provider entry
  ]
})
export class AppModule {}

The Angular HttpClient Service

The new Angular HttpClient API was introduced in Angular 4.3+. It is a better alternative to the existing HTTP API that lives in its own package @angular/common/http.

In Angular 5, the old HTTP client which lives in @angular/http is deprecated so Angular and Ionic 3 developers need to migrate their existing apps to use the new HttpClient API.

HttpClient has many changes and features over the old API, such as:

  • the response is a JSON object by default, so there's no need to manually parse it
  • the introduction of the requestProgress interface for listenning for download and upload operations progress
  • the introduction of the HttpInterceptor interface for creating interceptors--middlewares that can be placed in the Request/Response pipeline

The Angular HttpClient service is available as an injectable class which can be imported from @angular/common/http.

HttpClient provides methods, for sending HTTP POST, GET, PUT and DELETE etc. requests, that return Observables.

An Example Implementation of Our Service Provider

Based on the endpoints exposed by our simple json-server back-end, we can create an example implementation of our Angular service

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class RestProvider {
  baseUrl:string = "http://localhost:3000";

  constructor(private httpClient : HttpClient) { }

  // Sending a GET request to /products
  public getProducts(){
  }

  // Sending a POST request to /products
  public createProduct(product: Product) {
  }

  // Sending a GET request to /products/:id
  public getProductById(productId: number) {
  }

  // Sending a PUT request to /products/:id
  public updateProduct(product: Product){
  }

  // Sending a DELETE request to /products/:id
  public deleteProductById(productId: number) { 
  }  

}

We have imported the Injectable decorator to transform this TypeScript class into an injectable service. Then we imported the HttpClient to make the HTTP requests.

Next we have declared the baseUrl variable to hold the address of your back-end API. Next we injected HttpClient as httpClient.

Before you can succesfully implement the service methods, you need to make sure to import the following dependencies from the RxJS library:

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';

You also need to declare and define a Product model, either in the same file as the service or in a separate file then import it:

export class Product {
   id: number;
   name: string;
   cost: number;
   quantity: number;
   constructor(values: Object = {}) {
        Object.assign(this, values);
   }
} 

Let's now see how to implement each one of these methods.

Implementing getProducts() for Getting All Products

The getProducts() method will be used to get all products from the corresponding API endpoint:

public getProducts(): Observable<Product[]> {
  return this.httpClient
    .get(this.baseUrl + '/products')
    .map(products => {
      return products.map((product) => new Product(product));
    })
    .catch((err)=>{
        console.error(err);
    });
}

We first call the .get() method to send the GET request to the corresponding endpoint which will return an endpoint

We then use the RxJS map() operator in the returned Observable to convert it from Observable to Observable i.e an array of Products.

We also use the .catch() method to log any thrown errors.

Implementing getProductById() for Getting Single Products

The getProductById() will be used to get a single product by its id

public getProductById(productId: number): Observable<Product> {
  return this.httpClient
    .get(this.baseUrl + '/products/' + productId)
    .map(response => {
        return new Product(response);
    })
    .catch((err)=>{
        console.error(err);
    });
}

Implementing createProduct() for Creating New Products

The createProduct() method will be used to create a new product by sending a POST request, with the product data, to the corresponding endpoint.

public createProduct(product: Product): Observable<Product> {
  return this.httpClient
    .post(this.baseUrl + '/products', product)
    .map(response => {
      return new Product(response);
    })
    .catch((error)=>{
        console.error(error);
    });
}

Implementing updateProduct() for Updating Existing Products

The updateProduct() will be used to update a product by its id, by sending a PUT request to the corresponding endpoint then will convert the response to a new Product using the RxJS .map() operator.

public updateProduct(product: Product): Observable<Product> {
  return this.httpClient
    .put(this.baseUrl + '/products/' + product.id, product)
    .map(response => {
      return new Product(response);
    })
    .catch((err)=>{
        console.error(err);
    });
}

Implementing deleteProductById() for Deleting Products

The deleteProductById() method will be used to delete single products by id, by sending a DELETE request to the corresponding endpoint:

public deleteProductById(productId: number) {
  return this.httpClient
    .delete(this.baseUrl+ '/products/' + productId)
    .catch((e)=>{
        console.error(e);
    });
}

Using the Rest API Service

After implementing the service to interface with our REST back-end, let's now see how to use the service in our app.

All the methods we have previously implemented in the service return RxJS Observables

Calling any method in our components won't send any HTTP requests. We need to subscribe to the returned Observable to send the corresponding request to the API back-end.

To subscribe to an Observable, we need to use the .subscribe() method, which takes 3 arguments:

  • onNext: it's called when the Observable emits a new value
  • onError: it's called when the Observable throws an error
  • onCompleted: it's called when the Observable has gracefully terminated

Adding the Products Page

Use the Ionic CLI to generate a page for adding CRUD (Create, Read, Update and Delete) operations which will call the corresponding methods in the previously create service.

So first we need to import the service using:

import { RestProvider } from './../../providers/rest/rest';

Next we inject the service as restProvider:

  constructor(public navCtrl: NavController, public restProvider: RestProvider) { }

Next we declare an array to hold the products:

  private products : Product[] = []; 

Then we call this code, to get all products and store them in the products array, when the view enters or in the constructor:

    this.restProvider.getProducts().subscribe((products : Product[])=>{
      this.products = products;
    });

Next we need three methods to create, update and delete products:

onCreateProduct() is called when we need to create a product via a form. This method simply subscribes to the corresponding method in the service and concatenates the newly created product with the products array

  onCreateProduct(product) {
    this.restProvider
      .createProduct(product)
      .subscribe(
        (newProduct) => {
          this.products = this.products.concat(newProduct);
        }
      );
  }

onUpdateProduct() needs to be called when you need to update an existing product:

  onUpdateProduct(product) {
    this.restProvider
      .updateProduct(product)
      .subscribe(
        (updatedProduct) => {
          /* You can assign back the updated product to the model holding               the form's product*/
        }
      );
  }

onRemoveProduct() can be called when you need to delete a product. This method the array .filter() method to filter out the deleted product from the array of products:

  onRemoveProduct(product) {
    this.restProvider
      .deleteProductById(product.id)
      .subscribe(
        () => {
          this.products = this.products.filter((e) => e.id !== product.id);
        }
      );
  }

Unit Testing Angular Services with HttpClient

The HttpClientTestingModule allows you to easily mock HTTP requests by providing you with the HttpTestingController service. In this section we’ll see how you can create tests for the previously created service using the HttpTestingController service to mock requests instead of making real API requests to our API back-end when testing.

Before you can use HttpClientTestingModule and its HttpTestingController service you first need to import and provide them in your TestBed alongside the service we are testing.

So go ahead and create src/providers/rest/rest.spec.ts, which will hold code for testing the Rest service, then add the following code.

import { TestBed, getTestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

import { RestProvider } from './rest';

describe('RestProvider', () => {
  let injector: TestBed;
  let myProvider: RestProvider;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [RestProvider]
    });
    testBed = getTestBed();
    myProvider = testBed.get(RestProvider);
    httpMock = testBed.get(HttpTestingController);
  });
});

We are storing the provider and an instance of the HttpTestingController (httpMock) in variables so we can have access to them in each test that we run by using the beforEach(()=>{}) API.

Now let's test the getProducts() method as an example:

describe('getProducts', () => {
  it('should return an Observable<Product[]>', () => {
    const someProducts = [
      { id: 1, name : 'Product001', cost: 10 , quantity : 100 },
      { id: 2, name : 'Product002', cost: 100 , quantity : 200 },
      { id: 3, name : 'Product003', cost: 200 , quantity : 300 },
    ];

    myProvider.getProducts().subscribe((products) => {
      expect(products.length).toBe(3);
      expect(products).toEqual(someProducts);
    });

    const request = httpMock.expectOne(`${myProvider.baseUrl}/products`);
    expect(req.request.method).toBe("GET");

    request.flush(someProducts);
    httpMock.verify();
  });
});

Inside it('should return an Observable') we first define a varibale which holds some testing data then we call the provider's method (in this case .getProducts()) as we normally do. In the subscribe handler we tell Angular that we are expecting the retrun values which is products to equal to our someProducts array and that the length should equal to 3 (that's because we we are not using the real HttpClient but a mock based on HttpTestingController).

Next we tell the httpMock what's the HTTP method we expect to be sent and the endpoint'sURL.

Finally we fire the request with the data we use as a mock then we verify that there are no outstanding http requests.

You can follow the same steps for testing other HTTP methods i.e POST, PUT and DELETE or more accurately their corresponding operations in the service provider.

Conclusion

So we have implemented all the required methods to create a CRUD app with Ionic 3 and Angular 4.3+ HttpClient. ALl you need now is to link these methods to the HTML interfaces using list and form controls and some buttons.