JavaScript
Article
By Jurgen Van de Moere

Angular and RxJS: Create an API Service to Talk to a REST Backend

By Jurgen Van de Moere

Build an Angular CRUD App from the Ground Up

Two people holding a radio aerial, communicating with a giant Angular logo on a hill. Angular API service.

This article is part 3 of the SitePoint Angular 2+ Tutorial on how to create a CRUD App with the Angular CLI.


  1. Part 0— The Ultimate Angular CLI Reference Guide
  2. Part 1— Getting our first version of the Todo application up and running
  3. Part 2— Creating separate components to display a list of todo’s and a single todo
  4. Part 3— Update the Todo service to communicate with a REST API
  5. Part 4— Use Angular Router to resolve data
  6. Part 5— Add authentication to protect private content

In part one we learned how to get our Todo application up and running and deploy it to GitHub pages. This worked just fine but, unfortunately, the whole app was crammed into a single component.

In part two we examined a more modular component architecture and learned how to break this single component into a structured tree of smaller components that are easier to understand, re-use and maintain.

In this part, we will update our application to communicate with a REST API back-end.

You don’t need to have followed part one or two of this tutorial, for three to make sense. You can simply grab a copy of our repo, checkout the code from part two, and use that as a starting point. This is explained in more detail below.

A Quick Recap

Here is what our application architecture looked like at the end of part 2:

Application Architecture

Currently the TodoDataService stores all data in memory. In this third article, we will update our application to communicate with a REST API back-end instead.

We will:

  • create a mock REST API back-end
  • store the API URL as an environment variable
  • create an ApiService to communicate with the REST API
  • update the TodoDataService to use the new ApiService
  • update the AppComponent to handle asynchronous API calls
  • create an ApiMockService to avoid real HTTP calls when running unit tests

Application Architecture

By the end of this article, you will understand:

  • how you can use environment variables to store application settings
  • how you can use the Angular HTTP client to perform HTTP requests
  • how you can deal with Observables that are returned by the Angular HTTP client
  • how you can mock HTTP calls to avoid making real HTTP request when running unit tests

So, let’s get started!

Up and Running

Make sure you have the latest version of the Angular CLI installed. If you don’t, you can install this with the following command:

npm install -g @angular/cli@latest

If you need to remove a previous version of the Angular CLI, you can:

npm uninstall -g @angular/cli angular-cli
npm cache clean
npm install -g @angular/cli@latest

After that, you’ll need a copy of the code from part two. This is available at https://github.com/sitepoint-editors/angular-todo-app. Each article in this series has a corresponding tag in the repository so you can switch back and forth between the different states of the application.

The code that we ended with in part two and that we start with in this article is tagged as part-2. The code that we end this article with is tagged as part-3.

You can think of tags like an alias to a specific commit id. You can switch between them using git checkout. You can read more on that here.

So, to get up and running (the latest version of the Angular CLI installed) we would do:

git clone git@github.com:sitepoint-editors/angular-todo-app.git
cd angular-todo-app
git checkout part-2
npm install
ng serve

Then visit http://localhost:4200/. If all is well, you should see the working Todo app.

Setting up a REST API back-end

Let’s use json-server to quickly set up a mock back-end.

From the root of the application, run:

npm install json-server --save

Next, in the root directory of our application, create a file called db.json with the following contents:

{
  "todos": [
    {
      "id": 1,
      "title": "Read SitePoint article",
      "complete": false
    },
    {
      "id": 2,
      "title": "Clean inbox",
      "complete": false
    },
    {
      "id": 3,
      "title": "Make restaurant reservation",
      "complete": false
    }
  ]
} 

Finally, add a script to package.json to start our back-end:

"scripts": {
  ...
  "json-server": "json-server --watch db.json"
}

We can now launch our REST API using:

npm run json-server

which should display:

  \{^_^}/ hi!

  Loading db.json
  Done

  Resources
  http://localhost:3000/todos

  Home
  http://localhost:3000

That’s it! We now have a REST API listening on port 3000.

To verify that your back-end is running as expected, you can navigate your browser to http://localhost:3000.

The following endpoints are supported:

  • GET /todos: get all existing todo’s
  • GET /todos/:id: get an existing todo
  • POST /todos: create a new todo
  • PUT /todos/:id: update an existing todo
  • DELETE /todos/:id: delete an existing todo

so if you navigate your browser to http://localhost:3000/todos, you should see a JSON response with all todo’s from db.json.

To learn more about json-server, make sure to check out mock REST API’s using json-server.

Storing the API URL

Now that we have our back-end in place, we must store its URL in our Angular application.

Ideally, we should be able to:

  1. store the URL in a single place so that we only have to change it once when we need to change its value
  2. make our application connect to a development API during development and connect to a production API in production

Luckily, Angular CLI supports environments. By default, there are two environments: development and production, both with a corresponding environment file: src/environments/environment.ts and ‘src/environments/environment.prod.ts.

Let’s add our API URL to both files:

// src/environments/environment.ts
// used when we run `ng serve` or `ng build`
export const environment = {
  production: false,

  // URL of development API
  apiUrl: 'http://localhost:3000'
};
// src/environments/environment.prod.ts
// used when we run `ng serve --environment prod` or `ng build --environment prod`
export const environment = {
  production: true,

  // URL of production API
  apiUrl: 'http://localhost:3000'
};

This will later allow us to get the API URL from our environment in our Angular application by doing:

import { environment } from 'environments/environment';

// we can now access environment.apiUrl
const API_URL = environment.apiUrl;

When we run ng serve or ng build, Angular CLI uses the value specified in the development environment (src/environments/environment.ts).

But when we run ng serve --environment prod or ng build --environment prod, Angular CLI uses the value specified in src/environments/environment.prod.ts.

This is exactly what we need to use a different API URL for development and production, without having to change our code.

The application in this article series is not hosted in production, so we specify the same API URL in our development and production environment. This allows us to run ng serve --environment prod or ng build --environment prod locally to see if everything works as expected.

You can find the mapping between dev and prod and their corresponding environment files in .angular-cli.json:

"environments": {
  "dev": "environments/environment.ts",
  "prod": "environments/environment.prod.ts"
} 

You can also create additional environments such as staging by adding a key:

"environments": {
  "dev": "environments/environment.ts",
  "staging": "environments/environment.staging.ts",
  "prod": "environments/environment.prod.ts"
}

and creating the corresponding environment file.

To learn more about Angular CLI environments, make sure to check out the The Ultimate Angular CLI Reference Guide.

Now that we have our API URL stored in our environment, we can create an Angular service to communicate with the REST API.

Creating the Service to Communicate with the REST API

Let’s use Angular CLI to create an ApiService to communicate with our REST API:

ng generate service Api --module app.module.ts

which gives the following output:

installing service
  create src/app/api.service.spec.ts
  create src/app/api.service.ts
  update src/app/app.module.ts

The --module app.module.ts option tells Angular CLI to not only create the service but to also register it as a provider in the Angular module defined in app.module.ts.

Let’s open src/app/api.service.ts:

import { Injectable } from '@angular/core';

@Injectable()
export class ApiService {

  constructor() { }

} 

and inject our environment and Angular’s built-in HTTP service:

import { Injectable } from '@angular/core';
import { environment } from 'environments/environment';
import { Http } from '@angular/http';

const API_URL = environment.apiUrl;

@Injectable()
export class ApiService {

  constructor(
    private http: Http
  ) {
  }

}

Before we implement the methods we need, let’s have a look at Angular’s HTTP service.

If you’re unfamiliar with the syntax, why not buy our Premium course, Introducing TypeScript.

The Angular HTTP Service

The Angular HTTP service is available as an injectable class from @angular/http.

It is built on top of XHR/JSONP and provides us with an HTTP client that we can use to make HTTP requests from within our Angular application.

The following methods are available to perform HTTP requests:

  • delete(url, options): perform a DELETE request
  • get(url, options): perform a GET request
  • head(url, options): perform a HEAD request
  • options(url, options): perform an OPTIONS request
  • patch(url, body, options): perform a PATCH request
  • post(url, body, options): perform a POST request
  • put(url, body, options): perform a PUT request

Each of these methods returns an RxJS Observable.

In contrast to the AngularJS 1.x HTTP service methods, which returned promises, the Angular HTTP service methods return Observables.

Don’t worry if you are not yet familiar with RxJS Observables. We only need the basics to get our application up and running. You can gradually learn more about the available operators when your application requires them and the ReactiveX website offers fantastic documentation.

If you want to learn more about Observables, it may also be worth checking out SitePoint’s Introduction to Functional Reactive Programming with RxJS.

Implementing the ApiService Methods

If we think back of the endpoints our REST API back-end exposes:

  • GET /todos: get all existing todo’s

  • GET /todos/:id: get an existing todo

  • POST /todos: create a new todo

  • PUT /todos/:id: update an existing todo

  • DELETE /todos/:id: delete an existing todo

    we can already create a rough outline of methods we need and their corresponding Angular HTTP methods:

import { Injectable } from '@angular/core';
import { environment } from 'environments/environment';

import { Http, Response } from '@angular/http';
import { Todo } from './todo';
import { Observable } from 'rxjs/Observable';

const API_URL = environment.apiUrl;

@Injectable()
export class ApiService {

  constructor(
    private http: Http
  ) {
  }

  // API: GET /todos
  public getAllTodos() {
    // will use this.http.get()
  }

  // API: POST /todos
  public createTodo(todo: Todo) {
    // will use this.http.post()
  }

  // API: GET /todos/:id
  public getTodoById(todoId: number) {
    // will use this.http.get()
  }

  // API: PUT /todos/:id
  public updateTodo(todo: Todo) {
    // will use this.http.put()
  }

  // DELETE /todos/:id
  public deleteTodoById(todoId: number) {
    // will use this.http.delete()
  }
}

Let’s have a closer look at each of the methods.

getAllTodos()

The getAllTodos() method allows us to get all todos from the API:

public getAllTodos(): Observable<Todo[]> {
  return this.http
    .get(API_URL + '/todos')
    .map(response => {
      const todos = response.json();
      return todos.map((todo) => new Todo(todo));
    })
    .catch(this.handleError);
}

First, we make a GET request to get all todo’s from our API:

this.http
  .get(API_URL + '/todos')

This returns an Observable.

We then call the map() method on the Observable to transform the the response from the API into an array of Todo objects:

.map(response => {
  const todos = response.json();
  return todos.map((todo) => new Todo(todo));
})

The incoming HTTP response is a string, so we first call response.json() to parse the JSON string to its corresponding JavaScript value.

We then loop over the todo’s of the API response and return an array of Todo instances. Note that this second use of map() is using Array.prototype.map(), not the RxJS operator.

Finally, we attach an error handler to log potential errors to the console:

.catch(this.handleError);

We define the error handler in a separate method so we can reuse it in other methods:

private handleError (error: Response | any) {
  console.error('ApiService::handleError', error);
  return Observable.throw(error);
}

Before we can run this code, we must import the necessary dependencies from the RxJS library:

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

Note that the RxJS library is huge. Instead of importing the entire RxJS library using import * as Rx from 'rxjs/Rx', it is recommended to only import the pieces you require. This will substantially reduce the size of your resulting code bundle to a minimum.

In our application we import the Observable class:

import { Observable } from 'rxjs/Observable';

and import the three operators that our code requires:

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

Importing operators ensures that our Observable instances have the corresponding methods attached to them.

If we do not have import 'rxjs/add/operator/map' in our code, then the following would not work:

this.http
  .get(API_URL + '/todos')
  .map(response => {
    const todos = response.json();
    return todos.map((todo) => new Todo(todo));
  })

because the Observable returned by this.http.get would not have a map() method.

We only have to import the operators once to enable the corresponding Observable methods globally in your application. However, importing them more than once is not a problem and will not increase the resulting bundle size.

getTodoById()

The getTodoById() method allows us to get a single todo:

public getTodoById(todoId: number): Observable<Todo> {
  return this.http
    .get(API_URL + '/todos/' + todoId)
    .map(response => {
      return new Todo(response.json());
    })
    .catch(this.handleError);
}

We don’t need this method in our application, but it is included to give you an idea of what it would look like.

createTodo()

The createTodo() method allows us to create a new todo:

public createTodo(todo: Todo): Observable<Todo> {
  return this.http
    .post(API_URL + '/todos', todo)
    .map(response => {
      return new Todo(response.json());
    })
    .catch(this.handleError);
}

We first perform a POST request to our API and pass in the data as the second argument:

this.http.post(API_URL + '/todos', todo)

and then transform the response into a Todo object:

map(response => {
  return new Todo(response.json());
})

updateTodo()

The updateTodo() method allows us to update a single todo:

public updateTodo(todo: Todo): Observable<Todo> {
  return this.http
    .put(API_URL + '/todos/' + todo.id, todo)
    .map(response => {
      return new Todo(response.json());
    })
    .catch(this.handleError);
}

We first perform a PUT request to our API and pass in the data as the second argument:

put(API_URL + '/todos/' + todo.id, todo)

and then transform the response into a Todo object:

map(response => {
  return new Todo(response.json());
})

deleteTodoById()

The deleteTodoById() method allows us to delete a single todo:

public deleteTodoById(todoId: number): Observable<null> {
  return this.http
    .delete(API_URL + '/todos/' + todoId)
    .map(response => null)
    .catch(this.handleError);
}

We first perform a DELETE request to our API:

delete(API_URL + '/todos/' + todoId)

and then transform the response into null:

map(response => null)

We don’t really need to transform the response here and could leave out this line. It is just included to give you an idea of how you could process the response if you API would return data when you perform a DELETE request.

Here is the complete code for our ApiService:

import { Injectable } from '@angular/core';
import { environment } from 'environments/environment';

import { Http, Response } from '@angular/http';
import { Todo } from './todo';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

const API_URL = environment.apiUrl;

@Injectable()
export class ApiService {

  constructor(
    private http: Http
  ) {
  }

  public getAllTodos(): Observable<Todo[]> {
    return this.http
      .get(API_URL + '/todos')
      .map(response => {
        const todos = response.json();
        return todos.map((todo) => new Todo(todo));
      })
      .catch(this.handleError);
  }

  public createTodo(todo: Todo): Observable<Todo> {
    return this.http
      .post(API_URL + '/todos', todo)
      .map(response => {
        return new Todo(response.json());
      })
      .catch(this.handleError);
  }

  public getTodoById(todoId: number): Observable<Todo> {
    return this.http
      .get(API_URL + '/todos/' + todoId)
      .map(response => {
        return new Todo(response.json());
      })
      .catch(this.handleError);
  }

  public updateTodo(todo: Todo): Observable<Todo> {
    return this.http
      .put(API_URL + '/todos/' + todo.id, todo)
      .map(response => {
        return new Todo(response.json());
      })
      .catch(this.handleError);
  }

  public deleteTodoById(todoId: number): Observable<null> {
    return this.http
      .delete(API_URL + '/todos/' + todoId)
      .map(response => null)
      .catch(this.handleError);
  }

  private handleError (error: Response | any) {
    console.error('ApiService::handleError', error);
    return Observable.throw(error);
  }
}

Now that we have our ApiService in place, we can use it to let our TodoDataService communicate with our REST API back-end.

--ADVERTISEMENT--

Updating TodoDataService

Currently our TodoDataService stores all data in memory:

import {Injectable} from '@angular/core';
import {Todo} from './todo';

@Injectable()
export class TodoDataService {

  // Placeholder for last id so we can simulate
  // automatic incrementing of id's
  lastId: number = 0;

  // Placeholder for todo's
  todos: Todo[] = [];

  constructor() {
  }

  // Simulate POST /todos
  addTodo(todo: Todo): TodoDataService {
    if (!todo.id) {
      todo.id = ++this.lastId;
    }
    this.todos.push(todo);
    return this;
  }

  // Simulate DELETE /todos/:id
  deleteTodoById(id: number): TodoDataService {
    this.todos = this.todos
      .filter(todo => todo.id !== id);
    return this;
  }

  // Simulate PUT /todos/:id
  updateTodoById(id: number, values: Object = {}): Todo {
    let todo = this.getTodoById(id);
    if (!todo) {
      return null;
    }
    Object.assign(todo, values);
    return todo;
  }

  // Simulate GET /todos
  getAllTodos(): Todo[] {
    return this.todos;
  }

  // Simulate GET /todos/:id
  getTodoById(id: number): Todo {
    return this.todos
      .filter(todo => todo.id === id)
      .pop();
  }

  // Toggle todo complete
  toggleTodoComplete(todo: Todo) {
    let updatedTodo = this.updateTodoById(todo.id, {
      complete: !todo.complete
    });
    return updatedTodo;
  }

}

To let our TodoDataService communicate with our REST API, we must inject our new ApiService:

import { Injectable } from '@angular/core';
import { Todo } from './todo';
import { ApiService } from './api.service';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class TodoDataService {

  constructor(
    private api: ApiService
  ) {
  }
}

and update its methods to delegate all work to the corresponding methods in the ApiService:

import { Injectable } from '@angular/core';
import { Todo } from './todo';
import { ApiService } from './api.service';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class TodoDataService {

  constructor(
    private api: ApiService
  ) {
  }

  // Simulate POST /todos
  addTodo(todo: Todo): Observable<Todo> {
    return this.api.createTodo(todo);
  }

  // Simulate DELETE /todos/:id
  deleteTodoById(todoId: number): Observable<Todo> {
    return this.api.deleteTodoById(todoId);
  }

  // Simulate PUT /todos/:id
  updateTodo(todo: Todo): Observable<Todo> {
    return this.api.updateTodo(todo);
  }

  // Simulate GET /todos
  getAllTodos(): Observable<Todo[]> {
    return this.api.getAllTodos();
  }

  // Simulate GET /todos/:id
  getTodoById(todoId: number): Observable<Todo> {
    return this.api.getTodoById(todoId);
  }

  // Toggle complete
  toggleTodoComplete(todo: Todo) {
    todo.complete = !todo.complete;
    return this.api.updateTodo(todo);
  }

}

Our new method implementations look a lot simpler because the data logic is now handled by the REST API.

However, there is an important difference. The old methods contained synchronous code and immediately returned a value. The updated methods contain asynchronous code and return an Observable.

This means we also have to update the code that is calling the TodoDataService methods to handle Observables correctly.

Updating AppComponent

Currently, the AppComponent expects the TodoDataService to directly return JavaScript objects and arrays:

import {Component} from '@angular/core';
import {TodoDataService} from './todo-data.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [TodoDataService]
})
export class AppComponent {

  constructor(
    private todoDataService: TodoDataService
  ) {
  }

  onAddTodo(todo) {
    this.todoDataService.addTodo(todo);
  }

  onToggleTodoComplete(todo) {
    this.todoDataService.toggleTodoComplete(todo);
  }

  onRemoveTodo(todo) {
    this.todoDataService.deleteTodoById(todo.id);
  }

  get todos() {
    return this.todoDataService.getAllTodos();
  }

}

but our new ApiService methods return Observables.

Similar to Promises, Observables are asynchronous in nature, so we have to update the code to handle the Observable responses accordingly:

If we currently call the TodoDataService.getAllTodos() method in get todos():

// AppComponent

get todos() {
  return this.todoDataService.getAllTodos();
}

the TodoDataService.getAllTodos() method calls the corresponding ApiService.getAllTodos() method:

// TodoDataService

getAllTodos(): Observable<Todo[]> {
  return this.api.getAllTodos();
}

which, in turn, instructs the Angular HTTP service to perform an HTTP GET request:

// ApiService

public getAllTodos(): Observable<Todo[]> {
  return this.http
    .get(API_URL + '/todos')
    .map(response => {
      const todos = response.json();
      return todos.map((todo) => new Todo(todo));
    })
    .catch(this.handleError);
}

However, there is one important thing we have to remember!

As long as we don’t subscribe to the Observable returned by:

this.todoDataService.getAllTodos()

no actual HTTP request is made.

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

  • onNext: function that is called when the Observable emits a new value
  • onError: function that is called when the Observable throws an error
  • onCompleted: function that is called when the Observable has gracefully terminated

Let’s rewrite our current code:

// AppComponent

get todos() {
  return this.todoDataService.getAllTodos();
}

to load the todo’s asynchronously when the AppComponent is initialized:

import { Component, OnInit } from '@angular/core';
import { TodoDataService } from './todo-data.service';
import { Todo } from './todo';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [TodoDataService]
})
export class AppComponent implements OnInit {

  todos: Todo[] = [];

  constructor(
    private todoDataService: TodoDataService
  ) {
  }

  public ngOnInit() {
    this.todoDataService
      .getAllTodos()
      .subscribe(
        (todos) => {
          this.todos = todos;
        }
      );
  }
}

First, we define a public property todos and set its initial value to an empty array.

We then use the ngOnInit() method to subscribe to this.todoDataService.getAllTodos() and when a value comes in, we assign it to this.todos, overwriting its initial value of an empty array.

Now let’s update the onAddTodo(todo) method to also handle an Observable response:

// previously:
// onAddTodo(todo) {
//  this.todoDataService.addTodo(todo);
// }

onAddTodo(todo) {
  this.todoDataService
    .addTodo(todo)
    .subscribe(
      (newTodo) => {
        this.todos = this.todos.concat(newTodo);
      }
    );
}

Again, we use the subscribe() method to subscribe to the Observable returned by this.todoDataService.addTodo(todo) and when the response comes in, we add the newly created todo to the current list of todo’s.

We repeat the same exercise for the other methods until our AppComponent looks like this:

import { Component, OnInit } from '@angular/core';
import { TodoDataService } from './todo-data.service';
import { Todo } from './todo';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [TodoDataService]
})
export class AppComponent implements OnInit {

  todos: Todo[] = [];

  constructor(
    private todoDataService: TodoDataService
  ) {
  }

  public ngOnInit() {
    this.todoDataService
      .getAllTodos()
      .subscribe(
        (todos) => {
          this.todos = todos;
        }
      );
  }

  onAddTodo(todo) {
    this.todoDataService
      .addTodo(todo)
      .subscribe(
        (newTodo) => {
          this.todos = this.todos.concat(newTodo);
        }
      );
  }

  onToggleTodoComplete(todo) {
    this.todoDataService
      .toggleTodoComplete(todo)
      .subscribe(
        (updatedTodo) => {
          todo = updatedTodo;
        }
      );
  }

  onRemoveTodo(todo) {
    this.todoDataService
      .deleteTodoById(todo.id)
      .subscribe(
        (_) => {
          this.todos = this.todos.filter((t) => t.id !== todo.id);
        }
      );
  }
}

That’s it, all methods are now capable of handling Observables returned by the TodoDataService methods.

Notice that there is no need to unsubscribe manually when you subscribe to an Observable that is returned by the Angular HTTP service. Angular will clean up everything for you to prevent memory leaks.

Let’s see if everything is working as expected.

Trying it Out

Open a terminal window.

From the root of our application directory, start the REST API back-end:

npm run json-server

Open a second terminal window.

Again, from the root of our application directory, serve the Angular application:

ng serve

Now, navigate your browser to http://localhost:4200.

If all goes well, you should see:

If you see an error, you can compare your code to the working version on GitHub.

Awesome! Our application is now communicating with the REST API!

Side tip: if you want to run npm run json-server and ng serve in the same terminal, you can use concurrently to run both commands concurrently without opening multiple terminal windows or tabs.

Let’s run our unit tests to verify that everything is working as expected.

Running our tests

Open a third terminal window.

Again, from the root of your application directory, run the unit tests:

ng test

It seems that 11 unit tests are failing:

Let’s see why our tests are failing and how we can fix them.

Fixing our unit tests

First, let’s open up src/todo-data.service.spec.ts:

/* tslint:disable:no-unused-variable */

import {TestBed, async, inject} from '@angular/core/testing';
import {Todo} from './todo';
import {TodoDataService} from './todo-data.service';

describe('TodoDataService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [TodoDataService]
    });
  });

  it('should ...', inject([TodoDataService], (service: TodoDataService) => {
    expect(service).toBeTruthy();
  }));

  describe('#getAllTodos()', () => {

    it('should return an empty array by default', inject([TodoDataService], (service: TodoDataService) => {
      expect(service.getAllTodos()).toEqual([]);
    }));

    it('should return all todos', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
    }));

  });

  describe('#save(todo)', () => {

    it('should automatically assign an incrementing id', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getTodoById(1)).toEqual(todo1);
      expect(service.getTodoById(2)).toEqual(todo2);
    }));

  });

  describe('#deleteTodoById(id)', () => {

    it('should remove todo with the corresponding id', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
      service.deleteTodoById(1);
      expect(service.getAllTodos()).toEqual([todo2]);
      service.deleteTodoById(2);
      expect(service.getAllTodos()).toEqual([]);
    }));

    it('should not removing anything if todo with corresponding id is not found', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
      service.deleteTodoById(3);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
    }));

  });

  describe('#updateTodoById(id, values)', () => {

    it('should return todo with the corresponding id and updated data', inject([TodoDataService], (service: TodoDataService) => {
      let todo = new Todo({title: 'Hello 1', complete: false});
      service.addTodo(todo);
      let updatedTodo = service.updateTodoById(1, {
        title: 'new title'
      });
      expect(updatedTodo.title).toEqual('new title');
    }));

    it('should return null if todo is not found', inject([TodoDataService], (service: TodoDataService) => {
      let todo = new Todo({title: 'Hello 1', complete: false});
      service.addTodo(todo);
      let updatedTodo = service.updateTodoById(2, {
        title: 'new title'
      });
      expect(updatedTodo).toEqual(null);
    }));

  });

  describe('#toggleTodoComplete(todo)', () => {

    it('should return the updated todo with inverse complete status', inject([TodoDataService], (service: TodoDataService) => {
      let todo = new Todo({title: 'Hello 1', complete: false});
      service.addTodo(todo);
      let updatedTodo = service.toggleTodoComplete(todo);
      expect(updatedTodo.complete).toEqual(true);
      service.toggleTodoComplete(todo);
      expect(updatedTodo.complete).toEqual(false);
    }));

  });

});

Most of the failing unit tests are concerned with checking data handling. These tests are no longer required because data handling is now performed by our REST API instead of the TodoDataService, so let’s remove the obsolete tests:

/* tslint:disable:no-unused-variable */

import {TestBed, inject} from '@angular/core/testing';
import {TodoDataService} from './todo-data.service';

describe('TodoDataService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        TodoDataService,
      ]
    });
  });

  it('should ...', inject([TodoDataService], (service: TodoDataService) => {
    expect(service).toBeTruthy();
  }));

});

If we now run the unit tests, we get an error:

TodoDataService should ...
Error: No provider for ApiService!

The error is thrown because TestBed.configureTestingModule() creates a temporary module for testing and the injector of the temporary module is not aware of any ApiService.

To make the injector aware of the ApiService, we have to register it with the temporary module by listing ApiService as a provider in the configuration object that is passed to TestBed.configureTestingModule():

/* tslint:disable:no-unused-variable */

import {TestBed, inject} from '@angular/core/testing';
import {TodoDataService} from './todo-data.service';
import { ApiService } from './api.service';

describe('TodoDataService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        TodoDataService,
        ApiService
      ]
    });
  });

  it('should ...', inject([TodoDataService], (service: TodoDataService) => {
    expect(service).toBeTruthy();
  }));

});

However, if we do this, our unit test will use our real ApiService, which connects to our REST API.

We don’t want our test runner to connect to a real API when running our unit tests, so let’s create an ApiMockService to mock the real ApiService in unit tests.

Creating an ApiMockService

Let’s use Angular CLI to generate a new ApiMockService:

ng g service ApiMock --spec false

which shows:

installing service
  create src/app/api-mock.service.ts
  WARNING Service is generated but not provided, it must be provided to be used

Next, we implement the same methods as ApiService, but we let the methods return mock data instead of making HTTP requests:

import { Injectable } from '@angular/core';
import { Todo } from './todo';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';

@Injectable()
export class ApiMockService {

  constructor(
  ) {
  }

  public getAllTodos(): Observable<Todo[]> {
    return Observable.of([
      new Todo({id: 1, title: 'Read article', complete: false})
    ]);
  }

  public createTodo(todo: Todo): Observable<Todo> {
    return Observable.of(
      new Todo({id: 1, title: 'Read article', complete: false})
    );
  }

  public getTodoById(todoId: number): Observable<Todo> {
    return Observable.of(
      new Todo({id: 1, title: 'Read article', complete: false})
    );
  }

  public updateTodo(todo: Todo): Observable<Todo> {
    return Observable.of(
      new Todo({id: 1, title: 'Read article', complete: false})
    );
  }

  public deleteTodoById(todoId: number): Observable<null> {
    return null;
  }
}

Notice how each method returns fresh new mock data. This may seem a bit repetitive, but is a good practice. If one unit test would change mock data, the change can never affect the data in another unit test.

Now that we have an ApiMockService service, we can substitute ApiService in our unit tests with ApiMockService.

Let’s open up src/todo-data.service.spec.ts again.

In the providers array, we tell the injector to provide the ApiMockService whenever the ApiService is requested:

/* tslint:disable:no-unused-variable */

import {TestBed, inject} from '@angular/core/testing';
import {TodoDataService} from './todo-data.service';
import { ApiService } from './api.service';
import { ApiMockService } from './api-mock.service';

describe('TodoDataService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        TodoDataService,
        {
          provide: ApiService,
          useClass: ApiMockService
        }
      ]
    });
  });

  it('should ...', inject([TodoDataService], (service: TodoDataService) => {
    expect(service).toBeTruthy();
  }));

});

If we now re-run the unit tests, the error is gone. Great!

We still have 2 more failing tests though:

ApiService should ...
Error: No provider for Http!

AppComponent should create the app
Failed: No provider for ApiService!

The errors are similar to the one we just fixed.

To fix the first error, let’s open src/api.service.spec.ts:

import { TestBed, inject } from '@angular/core/testing';

import { ApiService } from './api.service';

describe('ApiService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [ApiService]
    });
  });

  it('should ...', inject([ApiService], (service: ApiService) => {
    expect(service).toBeTruthy();
  }));
});

The test fails with a message No provider for Http!, indicating that we need to add a provider for Http.

Again, we don’t want the Http service to send out real HTTP requests, so we instantiate a mock Http service that uses Angular’s MockBackend:

import { TestBed, inject } from '@angular/core/testing';

import { ApiService } from './api.service';
import { BaseRequestOptions, Http, XHRBackend } from '@angular/http';
import { MockBackend } from '@angular/http/testing';

describe('ApiService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: Http,
          useFactory: (backend, options) => {
            return new Http(backend, options);
          },
          deps: [MockBackend, BaseRequestOptions]
        },
        MockBackend,
        BaseRequestOptions,
        ApiService
      ]
    });
  });

  it('should ...', inject([ApiService], (service: ApiService) => {
    expect(service).toBeTruthy();
  }));
});

Don’t worry if configuring the test module looks a bit overwhelming.

You can learn more about setting up unit test in the official documentation for testing Angular applications.

To fix the final error:

AppComponent should create the app
Failed: No provider for ApiService!

let’s open up src/app.component.spec.ts:

import { TestBed, async } from '@angular/core/testing';

import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TodoDataService } from './todo-data.service';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        FormsModule
      ],
      declarations: [
        AppComponent
      ],
      providers: [
        TodoDataService
      ],
      schemas: [
        NO_ERRORS_SCHEMA
      ]
    }).compileComponents();
  }));

  it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));
});

and provide the injector with our mock ApiService:

import { TestBed, async } from '@angular/core/testing';

import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TodoDataService } from './todo-data.service';
import { ApiService } from './api.service';
import { ApiMockService } from './api-mock.service';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        FormsModule
      ],
      declarations: [
        AppComponent
      ],
      providers: [
        TodoDataService,
        {
          provide: ApiService,
          useClass: ApiMockService
        }
      ],
      schemas: [
        NO_ERRORS_SCHEMA
      ]
    }).compileComponents();
  }));

  it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));
});

Hurray! All our tests are passing:

We have successfully connected our Angular application to our REST API back-end.

To deploy our application to a production environment, we can now run:

ng build --aot --environment prod

and upload the generated dist directory to our hosting server. How sweet is that?

Let’s recap what we have learned.

Summary

In the first article, we learned how to:

  • initialize our Todo application using Angular CLI
  • create a Todo class to represent individual todo’s
  • create a TodoDataService service to create, update and remove todo’s
  • use the AppComponent component to display the user interface
  • deploy our application to GitHub pages

In the second article, we refactored AppComponent to delegate most of its work to:

  • a TodoListComponent to display a list of todo’s
  • a TodoListItemComponent to display a single todo
  • a TodoListHeaderComponent to create a new todo
  • a TodoListFooterComponent to show how many todo’s are left

In this third article, we:

  • created a mock REST API back-end
  • stored the API URL as an environment variable
  • created an ApiService to communicate with the REST API
  • updated the TodoDataService to use the new ApiService
  • updated the AppComponent to handle asynchronous API calls
  • created an ApiMockService to avoid real HTTP calls when running unit tests

In the process, we learned:

  • how to use environment variables to store application settings
  • how to use the Angular HTTP client to perform HTTP requests
  • how to deal with Observables that are returned by the Angular HTTP client
  • how to mock HTTP calls to avoid real HTTP requests when running unit tests

All code from this article is available at https://github.com/sitepoint-editors/angular-todo-app/tree/part-3.

In part four, we will introduce the router and refactor AppComponent to use the router to fetch the todo’s from the back-end.

In part five, we will implement authentication to prevent unauthorized access to our application.

So stay tuned for more!

This article was peer reviewed by Vildan Softic. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account