Data driven NodeJS tutorials Part 4 - Using and interacting with services

Hi all,

This is the fourth part of my tutorial series which focuses on NodeJS in a purely data-driven fashion - for those who like to keep their backends and front-ends distinct, this series should provide a quick kickstart.

I'm pushing the code from each tutorial into this public git repo:

https://github.com/philhudson91/data-driven-nodejs-tutorials

This part of the tutorial will provide the basics for using services. Services help to properly structure your application - minimising state, increasing testability and focusing on more functional programming.

Let's get started then!

Step 1 - Find your code from last time

For the fourth part, we will be re-using and re-working the code from the last part of the tutorial series.

You can find the previous part here, and all of the code on Github here.

Once you've got the code in front of you, make sure it's all okay by running:

mocha - to re-run the test.

node app.js - to check the app runs.

If all is well, let's move onto the next step.

Step 2 - Making the service

Create the file services/CatService.js

In terms of the patterns we will use, the file will not look too different from the controller - it will contain methods exposed via the exports function and usually (but not in this example) will contain some imports at the top of the file.

So let's now add the following code to the file:

exports.amountOfCats = function () {
    return Math.floor((Math.random() * 10) + 1);
};

exports.fluffiness = function () {
    return Math.floor((Math.random() * 10) + 1);
};

The code is pretty basic and we can see that it merely replicates the functions used to create the fluffiness and amountOfCats properties within our mainController.js file. But this is a good thing, as we will now modify the the controller to make use of the service, instead of Math.floor((Math.random() * 10) + 1); in the controller.

Step 3 - Wiring the service into the controller

Now open up controllers/mainController.js - we will start to integrate the service.

Let's import the CatService to be used in our controller - adding this line to the top of the file as usual:

var CatService = require('../services/CatService.js');

Now in our defaultEndpoint function, we can make use of the service instead of directly calling the Math function; we can change the code from this:

var amountOfCats = function (callback) {

        var amount = Math.floor((Math.random() * 10) + 1);

        if (amount) {
            callback(null, amount);
        } else {
            callback('Error cat amount failed', null);
        }

};

To this:

var amountOfCats = function (callback) {

        var amount = CatService.amountOfCats();

        if (amount) {
            callback(null, amount);
        } else {
            callback('Error cat amount failed', null);
        }

};

We can do the same with the catCreate fluffiness Math function too; from:

var fluffiness = function (callback) {

    var amount = Math.floor((Math.random() * 10) + 1);

    if (amount) {
        return callback(null, amount);
    } else {
        return callback('Error fluffiness amount failed', null);
    }

};

To:

var fluffiness = function (callback) {

    var amount = CatService.fluffiness();

    if (amount) {
        return callback(null, amount);
    } else {
        return callback('Error fluffiness amount failed', null);
    }

};

If we test it using mocha or node app.js in the browser, you'll see that the app behaves exactly like before. This is good - it means our refactor was successful.

So what is the point of using a service?

Testability, re-usability, cleaner code, SOLID principles, etc. etc. TL;DR - it's good for your code quality.

So let's re-use part of the service.

In the defaultEndpoint method, let's change the concatData function to make a call to the service, to return the average fluffiness of the cats!

Code below:

var concatData = function (amount, callback) {

    var dataToReturn = 'Phil has ' + amount.toString() + ' cats! The average fluffiness is: ' + CatService.fluffiness().toString();

    if (dataToReturn) {
        callback(null, dataToReturn);
    } else {
        callback('Data to return failed to process', null);
    }

};

You can see we've added:

The average fluffiness is: ' + CatService.fluffiness().toString();

to the dataToReturn string. Ideally I would add another async waterfall method and handle it properly, but this is just for demonstration purposes.

Now, we are using the CatService.fluffiness() function in multiple places. If I needed to change the function to return a scale from 1 - 20, it would be simple, I could edit it in one place in the controller and modify the tests that test it accordingly. Whereas if I used multiple Math.floor((Math.random() * 10) + 1); functions, they would be harder to find (in large applications there could be many, many places that would need a change, combing through code leads to errors) and they would unlikely be tested.

Now we can start to see the benefits of using services.

Let's test it again using mocha or node app.js in the browser:

{
message: "Phil has 7 cats! The average fluffiness is: 10"
}

Woo!

Now let's make extra use of the new service composition by writing a test for it.

Step 4 - Testing the service

Now, let's create a file tests/CatServiceTest.js.

Because we are testing the service directly and not the API we can get rid of Supertest, API and Expect from our imports.

For our imports, we simply include Chai's assertion module and the CatService file with the code below:

var assert = require('chai').assert;
var CatService = require('../services/CatService.js');

Now what do we want to test?

Basically we want to ensure that the amountOfCats function returns a number between 1 and 10. We do this below, using a similar pattern to the previous tests in the other part of the tutorial.

describe('amount of cats', function () {

    it('returns value between 1 and 10', function (done) {

        var amountOfCats = CatService.amountOfCats();

        assert(amountOfCats > 0, 'amountOfCats greater than 0');
        assert(amountOfCats <= 10, 'amountOfCats less than 10');

        done();
    });

});

Pretty standard! We describe and state what the test case should do, call the CatService.amountOfCats() method and assert that it's value is between 1 and 10.

We can then essentially duplicate the code for the fluffiness() method:

describe('fluffiness', function () {

    it('returns value between 1 and 10', function (done) {

        var fluffiness = CatService.fluffiness();

        assert(fluffiness > 0, 'fluffiness greater than 0');
        assert(fluffiness <= 10, 'fluffiness less than 10');

        done();
    });

});

And if we run mocha we should see:

 amount of cats
    ✓ returns value between 1 and 10

fluffiness
   ✓ returns value between 1 and 10

get main endpoint
  ✓ responds with 200

get cat add endpoint
  ✓ responds with 200


4 passing (37ms)

All is working! Woo.

Summary

Nice, we refactored our app to use a service, improving the code quality!

As usual, please comment or tweet me if you have any questions.

See all the parts below:

Part 1 - Basic server with test

Part 2 - Using controllers and basic async operations

Part 3 - Integrating models, Mongoose and MongoDB

And the code on Github here.

Phil Hudson

Read more posts by this author.