Consuming Externals Services

Angular has built-in support for communication with remote HTTP servers. The $http service handles low-level AJAX requests via the browser’s XMLHttpRequest object or via JSONP. The $resource service lets you interact with RESTful data sources and provides high-level behaviors which naturally map to RESTful resources.

Requesting JSON Data with AJAX

Problem

You wish to fetch JSON data via AJAX request and render it.

Solution

Implement a controller using the $http service to fetch the data and store it in the scope.

 1 <body ng-app="MyApp">
 2   <div ng-controller="PostsCtrl">
 3     <ul ng-repeat="post in posts">
 4       <li>{{post.title}}</li>
 5     </ul>
 6   </div>
 7 </body>
 8 
 9 var app = angular.module("MyApp", []);
10 
11 app.controller("PostsCtrl", function($scope, $http) {
12   $http.get('data/posts.json').
13     success(function(data, status, headers, config) {
14       $scope.posts = data;
15     }).
16     error(function(data, status, headers, config) {
17       // log error
18     });
19 });

You can find the complete example using the angular-seed project on github.

Discussion

The controller defines a dependency to the $scope and the $http module. An HTTP GET request to the data/posts.json endpoint is carried out with the get method. It returns a $promise object with a success and an error method. Once successful, the JSON data is assigned to $scope.posts to make it available in the template.

The $http service supports the HTTP verbs get, head, post, put, delete and jsonp. We are going to look into more examples in the following chapters.

The $http service automatically adds certain HTTP headers like for example X-Requested-With: XMLHttpRequest. But you can also set custom HTTP headers by yourself using the $http.defaults function:

1 $http.defaults.headers.common["X-Custom-Header"] = "Angular.js"

Until now the $http service does not really look particularly special. But if you look into the documentation you find a whole lot of nice features like, for example, request/response transformations, to automatically deserialize JSON for you, response caching, response interceptors to handle global error handling, authentication or other preprocessing tasks and, of course, promise support. We will look into the $q service, Angular’s promise/deferred service in a later chapter.

Consuming RESTful APIs

Problem

You wish to consume a RESTful data source.

Solution

Use Angular’s high-level $resource service. Note that the Angular ngResource module needs to be separately loaded since it is not included in the base angular.js file:

1 <script src="angular-resource.js">

Let us now start by defining the application module and our Post model as an Angular service:

1 var app = angular.module('myApp', ['ngResource']);
2 
3 app.factory("Post", function($resource) {
4   return $resource("/api/posts/:id");
5 });

Now we can use our service to retrieve a list of posts inside a controller (example: HTTP GET /api/posts):

1 app.controller("PostIndexCtrl", function($scope, Post) {
2   Post.query(function(data) {
3     $scope.posts = data;
4   });
5 });

Or a specific post by id (example: HTTP GET /api/posts/1):

1 app.controller("PostShowCtrl", function($scope, Post) {
2   Post.get({ id: 1 }, function(data) {
3     $scope.post = data;
4   });
5 });

We can create a new post using save (example: HTTP POST /api/posts):

1 Post.save(data);

And we can delete a specific post by id (example: DELETE /api/posts/1):

1 Post.delete({ id: id });

The complete example code is based on Brian Ford’s angular-express-seed and uses the Express framework.

You can find the complete example on github.

Discussion

Following some conventions simplifies our code quite a bit. We define the $resource by passing the URL schema only. This gives us a handful of nice methods including query, get, save, remove and delete to work with our resource. In the example above we implement several controllers to cover the typical use cases. The get and query methods expect three arguments, the request parameters, the success and the error callback. The save method expects four arguments, the request parameters, the POST data, the success and the error callback.

The $resource service currently does not support promises and therefore has a distinctly different interface to the $http service. But we don’t have to wait very long for it, since work has already started in the 1.1 development branch to introduce promise support for the $resource service!

The returned object of a $resource query or get function is a $resource instance which provides $save, $remove and $delete methods. This allows you to easily fetch a resource and update it as in the following example:

1 var post = Post.get({ id: 1 }, function() {
2   post.title = "My new title";
3   post.$save();
4 });

It is important to notice that the get call immediately returns an empty reference - in our case the post variable. Once the data is returned from the server the existing reference is populated and we can change our post title and use the $save method conveniently.

Note that having an empty reference means that our post will not be rendered in the template. Once the data is returned though, the view automatically re-renders itself showing the new data.

Configuration

What if your response of posts is not an array but a more complex json? This typically results in the following error:

1 TypeError: Object #<Resource> has no method 'push'

Angular seems to expect your service to return a JSON array. Have a look at the following JSON example, which wraps a posts array in a JSON object:

 1 {
 2   "posts": [
 3     {
 4       "id"    : 1,
 5       "title" : "title 1"
 6     },
 7     {
 8       "id": 2,
 9       "title" : "title 2"
10     }
11   ]
12 }

In this case you have to change the $resource definition accordingly.

 1 app.factory("Post", function($resource) {
 2   return $resource("/api/posts/:id", {}, {
 3     query: { method: "GET", isArray: false }
 4   });
 5 });
 6 
 7 app.controller("PostIndexCtrl", function($scope, Post) {
 8   Post.query(function(data) {
 9     $scope.posts = data.posts;
10   });
11 });

We only change the configuration of the query action to not expect an array by setting the isArray attribute to false. Then in our controller we can directly access data.posts.

It is generally good practice to encapsulate your model and $resource usage in an Angular service module and inject that in your controller. This way you can easily reuse the same model in different controllers and test it more easily.

Consuming JSONP APIs

Problem

You wish to call a JSONP API.

Solution

Use the $resource service and configure it to use JSONP. As an example we will take the Twitter search API here.

The HTML template lets you enter a search term in an input field and will render the search result in a list.

1 <body ng-app="MyApp">
2   <div ng-controller="MyCtrl">
3     <input type="text" ng-model="searchTerm" placeholder="Search term">
4     <button ng-click="search()">Search</button>
5     <ul ng-repeat="tweet in searchResult.results">
6       <li>{{tweet.text}}</li>
7     </ul>
8   </div>
9 </body>

The $resource configuration can be done in a controller requesting the data:

 1 var app = angular.module("MyApp", ["ngResource"]);
 2 
 3 function MyCtrl($scope, $resource) {
 4   var TwitterAPI = $resource("http://search.twitter.com/search.json",
 5     { callback: "JSON_CALLBACK" },
 6     { get: { method: "JSONP" }});
 7 
 8   $scope.search = function() {
 9     $scope.searchResult = TwitterAPI.get({ q: $scope.searchTerm });
10   };
11 }

You can find the complete example on github.

Discussion

The Twitter search API supports a callback attribute for the JSON format as described in their documentation. The $resource definition sets the callback attribute to JSON_CALLBACK, which is a convention from Angular when using JSONP. It is a placeholder that is replaced with the real callback function, generated by Angular. Additionally, we configure the get method to use JSONP. Now, when calling the API we use the q URL parameter to pass the entered searchTerm.

Deferred and Promise

Problem

You wish to synchronize multiple asynchronous functions and avoid Javascript callback hell.

Solution

As an example, we want to call three services in sequence and combine the result of them. Let us start with a nested approach:

 1 tmp = [];
 2 
 3 $http.get("/app/data/first.json").success(function(data) {
 4   tmp.push(data);
 5   $http.get("/app/data/second.json").success(function(data) {
 6     tmp.push(data);
 7     $http.get("/app/data/third.json").success(function(data) {
 8       tmp.push(data);
 9       $scope.combinedNestedResult = tmp.join(", ");
10     });
11   });
12 });

We call the get function three times to retrieve three JSON documents each with an array of strings. We haven’t even started adding error handling but already using nested callbacks the code becomes messy and can be simplified using the $q service:

 1 var first  = $http.get("/app/data/first.json"),
 2     second = $http.get("/app/data/second.json"),
 3     third  = $http.get("/app/data/third.json");
 4 
 5 $q.all([first, second, third]).then(function(result) {
 6   var tmp = [];
 7   angular.forEach(result, function(response) {
 8     tmp.push(response.data);
 9   });
10   return tmp;
11 }).then(function(tmpResult) {
12   $scope.combinedResult = tmpResult.join(", ");
13 });

You can find the complete example on github.

Discussion

The all function combines multiple promises into a single promise and solves our problem quite elegantly.

Let’s have a closer look at the then method. It is rather contrived but should give you an idea of how to use then to sequentially call functions and pass data along. Since the all function returns a promise again we can call then on it. By returning the tmp variable it will be passed along to the then function as tmpResult argument.

Before finishing this recipe let us quickly discuss an example where we have to create our own deferred object:

 1 function deferredTimer(success) {
 2   var deferred = $q.defer();
 3 
 4   $timeout(function() {
 5     if (success) {
 6       deferred.resolve({ message: "This is great!" });
 7     } else {
 8       deferred.reject({ message: "Really bad" });
 9     }
10   }, 1000);
11 
12   return deferred.promise;
13 }

Using the defer method we create a deferred instance. As an example of an asynchronous operation we will use the $timeout service which will either resolve or reject our operation depending on the boolean success parameter. The function will immediately return the promise and therefore not render any result in our HTML template. After one second, the timer will execute and return our success or failure response.

This deferredTimer can be triggered in our HTML template by wrapping it into a function defined on the scope:

 1 $scope.startDeferredTimer = function(success) {
 2   deferredTimer(success).then(
 3     function(data) {
 4       $scope.deferredTimerResult = "Successfully finished: " +
 5         data.message;
 6     },
 7     function(data) {
 8       $scope.deferredTimerResult = "Failed: " + data.message;
 9     }
10   );
11 };

Our startDeferredTimer function will get a success parameter which it passes along to the deferredTimer. The then function expects a success and an error callback as arguments which we use to set a scope variable deferredTimerResult to show our result.

This is just one of many examples of how promises can simplify your code, but you can find many more examples by looking into Kris Kowal’s Q implementation.

Testing Services

Problem

You wish to unit test your controller and service consuming a JSONP API.

Let’s have a look again at our example we wish to test:

 1 var app = angular.module("MyApp", ["ngResource"]);
 2 
 3 app.factory("TwitterAPI", function($resource) {
 4   return $resource("http://search.twitter.com/search.json",
 5     { callback: "JSON_CALLBACK" },
 6     { get: { method: "JSONP" }});
 7 });
 8 
 9 app.controller("MyCtrl", function($scope, TwitterAPI) {
10   $scope.search = function() {
11     $scope.searchResult = TwitterAPI.get({ q: $scope.searchTerm });
12   };
13 });

Note that it slightly changed from the previous recipe as the TwitterAPI is pulled out of the controller and resides in its own service now.

Solution

Use the angular-seed project and the $http_backend mocking service.

 1 describe('MyCtrl', function(){
 2   var scope, ctrl, httpBackend;
 3 
 4   beforeEach(module("MyApp"));
 5 
 6   beforeEach(
 7     inject(
 8       function($controller, $rootScope, TwitterAPI, $httpBackend) {
 9         httpBackend = $httpBackend;
10         scope = $rootScope.$new();
11         ctrl = $controller("MyCtrl", {
12           $scope: scope, TwitterAPI: TwitterAPI });
13 
14         var mockData = { key: "test" };
15         var url = "http://search.twitter.com/search.json?" +
16           "callback=JSON_CALLBACK&q=angularjs";
17         httpBackend.whenJSONP(url).respond(mockData);
18       }
19     )
20   );
21 
22   it('should set searchResult on successful search', function() {
23     scope.searchTerm = "angularjs";
24     scope.search();
25     httpBackend.flush();
26 
27     expect(scope.searchResult.key).toBe("test");
28   });
29 
30 });

You can find the complete example on github.

Discussion

Since we now have a clear separation between the service and the controller, we can simply inject the TwitterAPI into our beforeEach function.

Mocking with the $httpBackend is done as a last step in beforeEach. When a JSONP request happens we respond with mockData. After the search() is triggered we flush() the httpBackend in order to return our mockData.

Have a look at the ngMock.$httpBackend module for more details.