AngularJS Services and Factories Done Right

By AJ Hekman on 07 05 2014

This post is a continuation of AngularJS: a retrospective

We were tasked with creating a web application that was being developed simultaneously with an API from another team. Our AngularJS thick client web application had to be able to respond quickly to changes in business requirements, and changes in the API. Our priority of separating concerns and keeping single sources of truth served us well. We started by defining our own base data model class, which we called PropertyModel.

PropertyModel, our base data model class

Angular works with vanilla JavaScript objects, but plain objects can't introspect.
We wanted a standard interface for accessing both properties (object.property) and methods (object.method()). When we consume data, we don't want to worry about it's implementation. What we came up with is the PropertyModel. With a consistent interface, we didn't have to hunt down which parts of code requested a value that was once a simple property accessor is now a full blown method.
PropertyModel stores key value pairs on @properties, and accesses them with get/set methods.
Because we're accessing via a method, we can redirect calls with an appropriate method in @accessors prefixed with either get_ or set_.
It doesn't matter if it's a plain old property or a method; it will be accessed as `model.get('propName').

User model with accessor methods

get_fullName is an example composite property. There is no fullName on @properties.

get_gender is an example of intercepting a value lookup. Imagine that @properties.gender is normally present, but for some of our earlier users we forgot to ask for it. Here, we can make an educated guess based on @properties.title.

get_isTaxable is an example logic check for a business requirement. Perhaps your app needs to apply sales tax based on where the user lives. By having this check on the user model, when your requirements change, you can update the single source of truth. Note: Server side validation of user data is always necessary.

API service

Our services were responsible for all API calls. After a successful API call, a service creates a new user with the appropriate factory. For a failed call, the service logs the error to the console, then carries on the promise rejection with $q.reject without which the callee would not receive the error. When the API is updated the service may be the only part of your application that needs updating. How about that!

Retrieving the user from a controller

The controller only calls the service with the user ID it wants.