Chapter 6. AngularJS and REST Services

In the new era of mobile everywhere, the business logic for AngularJS applications should always be placed in REST services whenever possible. AngularJS applications should be kept clean and simple. Why? As AngularJS evolves over the next few years, it is very possible that most AngularJS applications will be rewritten.

This means that any business logic placed inside an AngularJS application will need to be rewritten as well—a serious consideration for applications containing large amounts of business logic. REST services, on the other hand, may be around for years to come. As web services technologies evolve, many REST services may undergo upgrades and modifications, but a complete service rewrite is unlikely in most cases. The best place for business logic is the place that will undergo the least amount of change and be available to all types of applications, now and in the future.

REST Services

REST (REpresentational State Transfer) services allow for a “separation of concerns.” REST services are not concerned with the user interface or user state, and clients that use REST services are not concerned with data storage or business logic. Clients can be developed independently of the REST services, as we have shown in previous chapters, using mock data. REST services can likewise be developed independently of the client, with no concern for client specifics or even the types of clients using the services. REST services should perform in the same way for all clients.

REST services should be stateless. A REST service should never hold data in a session variable. All information needed for a REST service call should be contained in the request and header passed from the client to the service. Any state should be held in the client and not in the service. There are many ways to hold state in an AngularJS application, including local storage, cookies, or cache storage.

A REST web service is said to be RESTful when it adheres to the following constrants:

  • It’s URL-based (e.g., http://www.micbutton.com/rs/blogPost).
  • It uses an Internet media type such as JSON for data interchange.
  • It uses standard HTTP methods (GET, PUT, POST, DELETE).

HTTP methods have a particular purpose when used with REST services. The following is the standard way that HTTP methods should be used with REST services:

  1. POST should be used to:
    1. Create a new resources.
    2. Retrieve a list of resources when a large amount of request data is required to be passed to the service.
  2. PUT should be used to update a resource.
  3. GET should be used to retrieve a resource or a list of resources.
  4. DELETE should be used to delete a resource.

For example, the following would be the proper use of HTTP methods:

  1. POST: http://www.micbutton.com/rs/blogPost to create a new blog post
  2. PUT: http://www.micbutton.com/rs/blogPost to update a blog post
  3. GET: http://www.micbutton.com/rs/blogPost/50 to get the blog post with id equal to 50
  4. DELETE: http://www.micbutton.com/rs/blogPost/50 to delete the blog post with id equal to 50

Ways to Communicate with REST Services

There are currently two ways to communicate with REST services using AngularJS:

The $http service
This service provides low-level interaction with REST services using the browser’s XMLHttpRequest object.
The $resource object
This object provides a high-level approach to interacting with REST services, simplifying the process considerably.

We will focus mostly on using the $resource object for communicating with REST services and leave the $http service discussion to other books (although we will use the $http service in later chapters for handling Basic Authentication headers). All our project code uses the $resource object.

The following code shows how to define an AngularJS service that can be used to interact with the BlogPost REST service. Notice that we pass the REST service URL to the $resource object. The methods defined match the REST services that are defined on that particular URL. Once the BlogPost service is defined, it can be used like a standard JavaScript object to access the different REST services defined on this URL:

/* chapter6/services.js */

'use strict';
/* Services */

var blogServices = 
 angular.module('blogServices', ['ngResource']);
 
blogServices.factory('BlogPost', ['$resource',
function($resource) {

return $resource("http://www.micbutton.com/rs/blogPost", {}, {
  get: {method: 'GET', cache: false, isArray: false},
  save: {method: 'POST', cache: false, isArray: false},
  update: {method: 'PUT', cache: false, isArray: false},
  delete: {method: 'DELETE', cache: false, isArray: false}
  });

}]);

Using the $resource object is by far the easiest way to call REST services. As you can see from this example, the AngularJS service code is straightforward and really fairly uncomplicated. Even when many services are defined, the services.js file is relatively simple.

The AngularJS $http service mentioned earlier is another way to call REST services. However, using the $http service would require many more lines of code related to REST service calls than we need using the $resource object. We do use the $http service in several places in the blog application, though, such as to send a Basic Authentication header to REST services. We will cover that in later chapters.

Updating the Project for REST

Before we can use our service, the new services.js file must be loaded at runtime and the new services module, blogServices, must be specified as a dependency of the application at startup time. Here is the line that should be added to the index.html file to load the services.js file:

/* chapter6/index.html excerpt */

<script src="js/services.js"></script>

And here is the complete index.html file, with this addition:

<!-- chapter6/index.html complete file -->

<!DOCTYPE html>
<html lang="en" ng-app="blogApp">

<head>
<title>AngularJS Blog</title>

<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">


<link rel="stylesheet" href="lib-css/bootstrap.min.css" media="screen"/>
<link rel="stylesheet" href="css/styles.css" media="screen"/>


<script src="js/libs/jquery-1.10.2.min.js"></script>
<script src="js/libs/bootstrap.min.js"></script>
<script src="js/libs/angular.min.js"></script>
<script src="js/libs/angular-route.min.js"></script>
<script src="js/libs/angular-resource.min.js"></script>
<script src="js/libs/angular-cookies.min.js"></script>

<script src="js/app.js"></script>
<script src="js/controllers.js"></script>
<script src="js/services.js"></script>


</head>

<body>
<div ng-view></div>
</body>

</html> 

The following code shows how we use inline annotations to add the new BlogServices module as a dependency of the application at startup time. Once the new module is added here, the services defined on the module can be used by any controller in the application:

/* chapter6/app.js */

'use strict';
/* App Module */

var blogApp = angular.module('blogApp', [
  'ngRoute',
  'blogControllers',
  'blogServices'
]);


blogApp.config(['$routeProvider', '$locationProvider',
  function($routeProvider, $locationProvider) {
    $routeProvider.
      when('/', {
      templateUrl: 'partials/main.html',
      controller: 'BlogCtrl'
    }).when('/blogPost/:id', {
      templateUrl: 'partials/blogPost.html',
      controller: 'BlogViewCtrl'
  });

  $locationProvider.html5Mode(false).hashPrefix('!');
}]);

REST Services and Controllers

Now let’s look at how to use the BlogPost service inside the BlogViewCtrl controller. First we must define the service as a requirement of the controller, as shown here. We then make a call to the get method and pass the id as an argument. We also define two callback functions, success and error (if you do not understand JavaScript callback functions, now would be a good time to stop and research how they work):

/* chapter6/controllers.js excerpt */

blogControllers.controller('BlogViewCtrl', 
  ['$scope', '$routeParams', 'BlogPost',

  function BlogViewCtrl($scope, $routeParams, BlogPost) {
    var blogId = $routeParams.id;
  
    BlogPost.get({id: blogId},
      function success(response) {
      console.log("Success:" + JSON.stringify(response));
      $scope.blogEntry = response;
    },
    function error(errorResponse) {
      console.log("Error:" + JSON.stringify(errorResponse));
    }
  );
}]);

When a call is made to the BlogViewCtrl controller, the id is retrieved from $routeParams. A call is then made to the get method of the BlogPost service, passing the id as an argument. At that point, the call to the controller completes.

Theoretically we don’t know when the REST service call will return results, but when it does, either the success callback function or the error callback function will be called. If the REST service call fails, the code inside the error callback function should handle the error condition. If the REST service call is successful, the code inside the success callback function handles the success functionality.

List Services

If we wanted a list of blog posts, we could define the following REST service: GET: http://www.micbutton.com/rs/blogList. Let’s take a look at how we would define that service in the services.js file. Notice that we specify isArray: true. This defines the service as returning a list and not an individual resource:

/* chapter6/services.js excerpt */

blogServices.factory('BlogList', ['$resource',
function($resource) {
  return 
$resource
("http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blogList", 
{}, {
    get: {method: 'GET', cache: false, isArray: true}
  });

}]);

Following is the controller code used to access the BlogList service. We inject the service into the controller as we did earlier, and like before, we pass success and error callback functions to the service call. The response from a successful service call is assigned to the blogList property of the scope and passed to the view:

/* chapter6/controllers.js excerpt */

blogControllers.controller('BlogCtrl', ['$scope', 'BlogList',
  function BlogCtrl($scope, BlogList) {

    BlogList.get({},
      function success(response) {
        console.log("Success:" + JSON.stringify(response));
        $scope.blogList = response;
    },
    function error(errorResponse) {
      console.log("Error:" + JSON.stringify(errorResponse));
    }
  );
}]);

We access the JSON inside the view by using the blogList scope property, as shown here. This is the same technique we used in Chapter 5. We use the ng-repeat directive to iterate over the list as before:

<!-- chapter6/main.html excerpt -->

<div ng-repeat="blogPost in blogList"> 

<div class="blog-post-outer"> 
<div class="blog-intro-text"> 

Posted: {{blogPost.date | date:'MM/dd/yyyy @ h:mma'}} </div> 
<div class="blog-intro-text"> {{blogPost.introText}} </div> 

<div class="blog-read-more"> 

<a href="#!blogPost/{{blogPost._id}}">Read More</a> 

</div>

Testing Services with Karma

The best way to test AngularJS services is with Karma. We used Karma as one of our test frameworks in previous chapters. Unit testing a service lets us validate that the unit of code that is used to build the service is working correctly. Unit testing an AngularJS service that connects to a REST service is a potential cause of errors, however.

REST service calls are asynchronous, so there can be a delay before the service call results are available to the part of the application that initiated the REST call. Considering that a REST service is not actually part of the unit of code that we would be testing with a unit test, we shouldn’t be too concerned about REST calls when unit testing.

Karma, as I mentioned before, should be the unit test framework for our blog application. The following code shows how we modify a normal Karma configuration file to allow us to test code where the AngularJS $resource object is used. Notice the line "public_html/js/libs/angular-resource.min.js". With that line, we tell Karma to use the AngularJS angular-resource.min.js file. That file is needed only when we’re working with code that calls REST services:

/* chapter6/karma.conf.js */

module.exports = function (config) {
    config.set({
        basePath: '../',
        files: [
            "public_html/js/libs/angular.min.js",
            "public_html/js/libs/angular-mocks.js",
            "public_html/js/libs/angular-route.min.js",
            "public_html/js/libs/angular-resource.min.js",            
            "public_html/js/*.js",
            "test/**/*Spec.js"
        ],
        exclude: [
        ],
        autoWatch: true,
        frameworks: [
            "jasmine"
        ],
        browsers: [
            "Chrome",
            "Firefox"
        ],
        plugins: [
            "karma-junit-reporter",
            "karma-chrome-launcher",
            "karma-firefox-launcher",
            "karma-jasmine"
        ]
    });
};

Karma Service Specifications

In order to test AngularJS services, we need to add a test specification specifically for the blog application services. The following code shows a servicesSpec.js file. The test specification has unit testing for two services. The first unit test is for the BlogList service, and the second test is for the BlogPost service:

/* chapter6/servicesSpec.js */

describe('AngularJS Blog Service Testing', function () {   

    describe('test BlogList', function () {       
        var $rootScope;
        var blogList;

        beforeEach(module('blogServices'));

        beforeEach(inject(function ($injector) {           
            $rootScope = $injector.get('$rootScope');
            blogList = $injector.get('BlogList');
        }));

        it('should test BlogList service', function () {            
            expect(blogList).toBeDefined();
        });

    });

     describe('test BlogPost', function () {       
        var $rootScope;
        var blogPost;

        beforeEach(module('blogServices'));

        beforeEach(inject(function ($injector) {           
            $rootScope = $injector.get('$rootScope');
            blogPost = $injector.get('BlogPost');
        }));

        it('should test BlogPost service', function () {            
            expect(blogPost).toBeDefined();
        });

    });

});

Notice in this code that we use $injector to inject the two services directly into the test scripts. As I mentioned earlier, we are not testing the REST services themselves; we are only testing the AngularJS services that connect to REST services. The tests should succeed even if the REST services are down for some reason.

End-to-End Testing

End-to-end testing done with Protractor is a much better way to test the functionality of REST services and the applications associated with them. Most modern software development teams use some type of continuous integration (CI) build system. Most CI systems can be configured to run end-to-end tests using Protractor.

Protractor E2E testing can even be configured to run tests against production environments. More often, however, E2E testing is written to run against services running on QA servers. E2E testing is a good way to test an application the same way a user would use the application.

Protractor Configuration

The following is a configuration file for Protractor. A specification file named blog-spec.js is referenced from the configuration file:

/* chapter6/conf.js Protractor configuration file */

exports.config = { 
  seleniumAddress: 'http://localhost:4444/wd/hub', 
  specs: ['e2e/blog-spec.js']
};

Protractor Test Specification

Let’s take a look at the contents of the blog-spec.js file. You can see that the browser.get(URL) call can be made against any accessible URL. The URL could point to a local development box, a QA server, or a production server. REST services can be thoroughly tested with a Protractor test script:

/* chapter6/blog-spec.js Protractor test specification */

describe("Blog Application Test", function(){
    it("should test the main blog page", function(){
        
        browser.get(
          "http://localhost:8383/AngularJsBlogChapter6/");
        expect(browser.getTitle()).toEqual("AngularJS Blog");
        
        //gets the blog list
        var blogList = 
           element.all(by.repeater('blogPost in blogList'));
        
        //tests the size of the blogList
        expect(blogList.count()).toEqual(1);
        
        
        browser.get(
          "http://localhost:8383/AngularJsBlogChapter6
           /#!/blogPost/5394e59c4f50850000e6b7ea");
        expect(browser.getTitle()).toEqual("AngularJS Blog");
        
        //gets the comment list
        var commentList = 
     element.all(by.repeater('comment in blogEntry.comments'));
        
        //checks the size of the commentList
        expect(commentList.count()).toEqual(2); 
    });
})