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 (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:
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:
POST
should be used to:
PUT
should be used to update a resource.GET
should be used to retrieve a resource or a list of resources.DELETE
should be used to delete a resource.For example, the following would be the proper use of HTTP methods:
POST: http://www.micbutton.com/rs/blogPost
to create a new blog postPUT: http://www.micbutton.com/rs/blogPost
to update a blog postGET: http://www.micbutton.com/rs/blogPost/50
to get the blog post with id
equal to 50DELETE: http://www.micbutton.com/rs/blogPost/50
to delete the blog post with id
equal to 50
AngularJS REST service calls are asynchronous Ajax calls based on the $q
service’s promise and deferred APIs. We will not cover promises, deferred objects, or Ajax in this book. If you do not understand how Ajax is used to make asynchronous calls, now would be a good time to research these topics. Making asynchronous Ajax REST service calls is not specific to AngularJS or any other client-side JavaScript framework. Many libraries provide Ajax functionality, including jQuery, Dojo, and others.
There are three ways to create and register services in AngularJS. They are as follows:
service
functionprovider
functionfactory
functionHere’s how to create a service with the service
function (we will not use this method to create services in this book):
/* chapter6/ service function */
var
blogServices
=
angular
.
module
(
'blogServices'
,
[
'ngResource'
]);
blogServices
.
service
(
'BlogPost'
,
[
…
]
You can also create services with the provider
function, as shown here:
/* chapter6/ provider function */
var
blogServices
=
angular
.
module
(
'blogServices'
,
[
'ngResource'
]);
blogServices
.
provider
(
'BlogPost'
,
[
…
]
The third way to create services in AngularJS is with the factory
function. This is the most commonly used method, and the method we will use to create AngularJS services throughout this book:
/* chapter6/ factory function */
var
blogServices
=
angular
.
module
(
'blogServices'
,
[
'ngResource'
]);
blogServices
.
factory
(
'BlogPost'
,
[
…
]
We will now look at how to connect to REST services in AngularJS, although we will not actually implement the service code in our blog application until Chapter 7. We need to get a good theoretical understanding of AngularJS services before we start coding. Once we have that understanding, we will be set for Chapter 7.
There are currently two ways to communicate with REST services using AngularJS:
$http
serviceXMLHttpRequest
object.$resource
objectWe 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.
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
(
'!'
);
}]);
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.
Now let’s take a look at the JSON response object returned upon success. If the REST service call is successful, we set the JSON returned as the value of a scope property named blogEntry
. The property is at that point bound to the view, and AngularJS updates the view with the new values that were retrieved from the REST service call. If the REST service call fails, the screen is not updated, but we log the error to the console to help diagnose the failure. The JSON response object returned from a successful call looks like this:
{
"chapter: 6,"
JSON
": "
response
"}
{
"
_id
":1,
"
date
":1400623623107,
"
introText
":"
This
is
a
blog
post
about
AngularJS.
We
will
cover
how
to
build
",
"
blogText
":"
This
is
a
blog
post
about
AngularJS.
We
will
cover
how
to
build
a
blog
and
how
to
add
comments
to
the
blog
post.
",
"
comments
":[
{
"
commentText
":"
Very
good
post.
I
love
it.
"
},
{
"
commentText
":"
When
can
we
learn
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>
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"
]
});
};
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 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.
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'
]
};
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
);
});
})
This concludes our discussion of REST service basics. Throughout the rest of this book we’ll be working with live REST services. As we proceed, you will gain a better understanding of REST service concepts. We will now start working with actual REST services created especially for this book.