Last Updated: February 25, 2016
·
4.14K
· wedgybo

Creating a reusable data service module for your API in AngularJS

I’ve been thinking about data services a lot, and it’s the one part of Angular that bothers me. I’ve since realised that it only bothers me because Angular doesn’t dictate or even really suggest how you should manage your data services. So I am always second guessing myself and wondering “am I doing this right?” Of course the answer is, if it works for you, then it’s right, because Angular doesn’t care. This has worked so far but it’s led to messy code that was hard to transfer to other projects. So we went back to the drawing board.

After reading various posts and watching videos on Rich data modelling in Angular, I still thought a lot of the solutions were a bit cumbersome. I'm not convinced we're right yet, but, we’ve come up with the following which is works well for our needs and seems to be a fairly decent approach, that’s not too mind boggling. I wanted to publish some code here to help out anyone who’s been feeling the same way I’d felt about the problem and to get feedback.

services.js

This is the base service that we will use. This allows us to include one module in our application to include all the services required to interact with the Mallzee API. The cool thing about this is it lets us create API calls for every service that extends it and keeps a lot of our code DRY.

We can create a lot of common calls here, such as fetch, fetchOne, query, etc. The main driver behind this approach was because we were repeating ourself writing cache merge code in our services. We want the ability to pull our data from LocalStorage first, do a request for the live data and update any changes. More on this in another post. This technique lets us do this once in our common calls.

We also create a provider so that this service and it's use of restangular can be coupled with other services without causing problems with base URLs and the like.

angular.module('mallzee.services', [
  'restangular',
  'mallzee.services.brands',
  'mallzee.services.products'
]).provider('MallzeeService', function MallzeeServiceProvider() {

    var baseUrl = 'https://api.mallzee.com';

    var scope = this;
    this.$get = ['MallzeeRestangular', function (MallzeeRestangular) {

      MallzeeRestangular.setBaseUrl(baseUrl);

      var MallzeeService = function () { };
      MallzeeService.prototype = {
        scope: this,
        initialise: function () {
          scope = this;

          if (scope.key) {
            this[scope.key] = [];
          }
          if (scope.key && scope.model) {
            // Extend any objects sourced from Local Storage
            angular.forEach(scope[scope.key], function (obj) {
              angular.extend(obj, scope.model);
            });

            // Extend any future objects we retrieve with Restangular
            MallzeeRestangular.extendModel(scope.key, function (model) {
              return angular.extend(model, scope.model);
            });
          }
        },
        fetch: function () {
          scope = this;
          MallzeeRestangular.all(scope.key).getList().then(function (data) {
            angular.extend(scope[scope.key], data);
          });
        },
        fetchOne: function() {} // Removed to keep things short
        query: function () {} // Removed to keep things short
      };

      return {
        getInstance: function () {
          return new MallzeeService();
        }
      }
    }];

    this.setBaseUrl = function (url) {
      baseUrl = url;
    };

  }).factory('MallzeeRestangular', ['Restangular', function (Restangular) {
    return Restangular.withConfig(function(RestangularConfigurator) {
      // Configure the version of Restangular used for this API
      // i.e. set headers, object transforms etc
    });
  }]);

services/brands.js

When we create our service, we just inject our base service MallzeeService and extend an instance of that to provide us with a service specific API.

We make use of the factory to give the items returned a model like structure, this is a perfect place to put business logic and keep it our of the controllers.

angular.module('mallzee.services.brands', [])
    // This will act as the model for each brand item received from the API
    .factory('Brand', [function () {
      return {
        toggle: function () {
          this.enabled = !this.enabled;
          return this.enabled;
        }
        // Add model specific API function here
      };
    }])
    .factory('BrandsService', ['MallzeeService', 'Brand', function (MallzeeService, Brand) {

      var brandsService = angular.extend(MallzeeService.getInstance(), {
        key: 'brands',
        model: Brand

        // Add any specific collection API functions to this object
      });
      brandsService.initialise();

      return brandsService;
    }]);

controllers/brands.js

We can then use this very simply in our controllers.

angular.module('mallzee.controllers.brands', [])
    .controller('BrandCtrl', [
    '$scope',
    'BrandsService',
    function ($scope, $state, BrandsService) {

      $scope.brands = BrandsService.brands;

      $scope.$on('$viewContentLoaded', function (event, view) {
        if (view.stateName === 'brands') {
          BrandsService.fetch();
        }
      });
    }]);

app.js

Finally we just include the top level service, and configure it if we want to point to a different URL say during development, or whatever.

angular.module('mallzee', [
    'mallzee.services'
).config(['MallzeeServiceProvider', function (MallzeeServiceProvider) {
    MallzeeServiceProvider.setBaseUrl('https://dev.mallzee.com');
});

I'm liking this because we should be able to dump this into any angular project now and be comfortable that our interactions are going to remain stable when talking with the API. It's also setup to work well with our caching system and local storage.

I'd love to get peoples thoughts on this.