Overview

5 Ways to Write Better Angular Services

No Comments

Angular services are singleton objects that can be used to organize and share code. Services are typically used to interact with REST endpoints, to organize common UI logic or even for business logic. During my Angular training and coaching sessions I advise people how to properly build and structure Angular services. This blog post describes these tips.

1: Write services using the ECMAScript 6 class notation

Angular 2.0 is going to be usable some time this year. Word on the street is that Google is even starting to migrate first applications to Angular 2.0 in May 2015! This means that the development of Angular 2.0 is coming around nicely and that we should prepare for it. While the Angular team is going to provide Angular 2.0 migration guides, one thing is already clear: Angular 2.0 is going to rely heavily on the ECMAScript 6 class notation.

ECMAScript 6 is the upcoming latest version of JavaScript which contains many improvements that the community has longed for. To name a few improvements:

  • block scoping
  • native modularization concept
  • class notation
  • destructuring
  • default parameters
  • rest parameters
  • arrow functions
  • Promises

You can read up on those features in Luke Hoban's es6featrue repository, Axel Rauschmayer's fantastic blog or refer to the specification. The remainder of this blog post uses ECMAScript 6 for all code listings and assumes that you know the basics about the class notation, arrow functions and Promises.

That Angular 2.0 is using the class notation is evident by the demos shown at ng-conf and the demos in the Angular 2.0 repository. So, why not start using ECMAScript 6 classes now and ease migration to Angular 2.0? Not only will the migration potentially be easier, but you will also be able to

  • use getters and setters with ease
  • use inheritance to share logic (although you obviously remember to try composition first)
  • use a syntax and structure that is defined by the language and thus is less reliant on a specific framework
const module = angular.module('es6Module', []);
 
class GreetingService {
 
  // DI annotations are possible via getters
  // static get $inject() {
  //  return ['$http'];
  // }
 
  getMessage() {
    return 'Hello World';
  }
}
module.service('GreetingService', GreetingService);
 
module.run(GreetingService => console.log(GreetingService.getMessage()));
 
angular.bootstrap(document.documentElement, [module.name]);

2: Services should be stateless

Stateful services, i.e. services which store temporary information in private or even public variables, are hard to reason about and are typically a code smell. Various issues arise when using stateful services:

  • What happens when the service is used by two components on the same page?
  • Is the state correctly reset at some point?
  • Is memory being leaked?
  • Is the page bookmarkable?
  • Are the browser controls, i.e. forward and backward buttons, behaving the way they are supposed to?
  • How can the service be unit tested?

I prefer to think of services as singleton objects that may be used concurrently and come and go at any time. If you get into this mindset, you will shy away from stateful services as much as possible.

3: Do not leak information about data sources

The principle of separation of concerns is dear to me. As such I hate to see Angular services expose the fact that HTTP requests are used to retrieve information (you might also call this a leaky abstraction).

Example: Suppose we have a GithubEventService that can be used to retrieve event streams, i.e. what has happened in repositories, organizations and so on. This information may be retrieved via a getEventsForOrganization(String orgName) method. Now, what should the return type of that method be? From a service consumer's point of view we are only interested in List<Event>, but due to the asynchronous nature of JavaScript, service consumers also need to be content with eventual results. Eventual results typically means Promises in the Angular ecosystem. This boils down to Promise<List<Event>>. Now, this return type is absolutely okay. We cannot hide asynchronous information retrieval.

Unfortunately, as mentioned above, some Angular services are not implemented with separation of concerns in mind. These services typically return a type like Promise<HttpResponse<List<Event>>>. Service consumers are not interested in the fact that HTTP requests are used under the hood. They only want a result. The following service implementation is hiding network communication via a Promise transformation with the following signature: <T> T hideNetworkCommunication(HttpResponse<T> response).

const hideNetworkCommunication = response => response.data;
 
const module = angular.module('hideDataSource', []);
 
class GithubEventService {
 
  constructor($http) {
    this.$http = $http;
  }
 
  getEventsForOrganization(orgName) {
    const encodedOrgName = encodeURIComponent(orgName);
    const url = `https://api.github.com/orgs/${encodedOrgName}/events`;
    return this.$http.get(url)
    .then(hideNetworkCommunication);
  }
}
 
module.service('GithubEventService', GithubEventService);
 
module.run(GithubEventService => {
  GithubEventService.getEventsForOrganization('codecentric')
  .then(events => console.log('GitHub events:', events));
});
 
angular.bootstrap(document.documentElement, [module.name]);

4: Handle errors in services

This tip is closely related to the third. Instead of handling the success cases, we are now turning to the error cases. Errors should be handled in services and transformed in such a way that controllers are straightforward to implement. Back to the Promise<List<Event>> getEventsForOrganization(String orgName) method.

When an error occurs during the HTTP request, Angular will reject the Promise with the HTTP response object as the reason. This is a leaky abstraction and we, as service authors, should handle these errors properly. Various errors can occur when using the $http service. For now, let us consider two common error cases and a generic one.

  1. The GitHub rate limit is exceeded. Anonymous users can send up to 60 requests per hour to the GitHub API. Once exceeded the GitHub API responds with a 403 status code.
  2. Events are retrieved for a GitHub organization. An organization may not exist and GitHub will return a 404 response in such cases.
  3. There may also be DNS resolution issues, network partitions or internal server errors. Due to the sheer number of possible problems and the lack of compensations, we choose a generic "please try again later" error.

Translation of errors can be done using Promise rejection handlers as the following code listing shows. Custom error types can be helpful and may be used to differentiate between generic, network and recoverable issues, i.e. recoverable through user interaction.

const hideNetworkCommunication = response => response.data;
 
const module = angular.module('hideDataSource', []);
 
class RateLimitExceededError extends Error {
  constructor() {
    super('Rate limit exceeded.');
  }
}
 
class RecoverableError extends Error {
  constructor(msg) {
    super(msg);
    this.recoverable = true;
  }
}
 
class GithubEventService {
 
  constructor($http, $q) {
    this.$http = $http;
    this.$q = $q;
  }
 
  getEventsForOrganization(orgName) {
    const encodedOrgName = encodeURIComponent(orgName);
    const url = `https://api.github.com/orgs/${encodedOrgName}/events`;
    return this.$http.get(url)
    .then(hideNetworkCommunication, response => {
      if (response.status === 403 &&
          response.headers('X-RateLimit-Remaining') === '0') {
        return this.$q.reject(new RateLimitExceededError());
      } else if (response.status === 404) {
        return this.$q.reject(new RecoverableError('Organization not found.'));
      }
 
      return this.$q.reject(new Error('An unknown error occurred.'));
    });
  }
}
 
module.service('GithubEventService', GithubEventService);
 
module.run(GithubEventService => {
  GithubEventService.getEventsForOrganization('unknownOrganisation')
  .then(null, error => console.error(error));
});
 
angular.bootstrap(document.documentElement, [module.name]);

5: Services should have a single responsibility

All services should have a single responsibility. Services that exceed this single responsibility should be split into multiple services. Not only will this keep you from ending up with monstrous 1000+ lines of code services, but you will also improve testability. Large services often have a lot of dependencies. Small services often have less dependencies than larger services. When unit testing, a small set of dependencies comes in handy as less dependencies need to stubbed or mocked!

With respect to our example: It would be naive to build one big GitHubService that contains methods for every possible GitHub API endpoint. Instead, smaller contexts should be established such as events, users, repository and search. This is not always easy, but pays off in the long run.

Comment

Your email address will not be published. Required fields are marked *