Data driven NodeJS tutorials Part 2 - Using controllers and basic async operations

Hi all,

This is the second 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

Let's get going on part 2!

Step 1 - Find your code from last time

For this second tutorial, 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 - Creating our first controller

Head to the root of your code and create a new folder called 'controllers'. If you want to do it in terminal, simply use:

mkdir controllers

Now we need to create the controller file, you can also do it in terminal using:

cd controllers; touch mainController.js

Now you have the controllers file in the controllers directory ready and waiting for some code. Let's do it!

mainController.js

exports.defaultEndpoint = function (req, res) {
    return res.status(200).json({message: "ok"});
};

What does this do? It creates a controller method called 'defaultEndpoint' and will return the same as what the routes.js file did.

Now we need to update our routes.js:

var express = require('express');
var router = express.Router();
var mainController = require('./controllers/mainController');

/* Map URLs to handlers in this file */

router.get('/', mainController.defaultEndpoint);

module.exports = router;

As you can see, the changes we have made simply import the mainController that we just created and then the method 'defaultEndpoint' is mapped to the router.get('/').

Now if you re-run the code using node app.js you should see it behave exactly like it did before. The tests should also be passing when you run mocha.

Why is this better though?

Simple, it helps us to break apart our code more, using a more Object Oriented approach our code will be easier to test, cleaner and faster to refactor.

Step 3 - Adding the Async module

Okay so this is still pretty basic. :)

Now we can get into some async stuff that's a little bit trickier.

Asynchronous simple means 'at the same time'. You will have to handle asynchronous code when working with Node, it is unavoidable.

Fortunately, a module available via NPM makes it pretty simple to handle.

Let's install the Async module.

Head to your package.json file and edit it to match the following:

{
  "name": "application-name",
  "version": "0.0.1",
  "dependencies": {
    "body-parser": "^1.0.2",
    "express": "^4.0.0",
    "request": "^2.67.0",
    "async": "^0.8.0"
  },
  "devDependencies": {
    "chai": "^1.9.1",
    "chance": "^0.5.9",
    "mocha": "^1.18.2",
    "node-inspector": "^0.12.3",
    "supertest": "^0.13.0"
  }
}

As you can see, we've added in the 'async' module into our package.json file.

Now if we run:

npm install

Async will be installed into your node_modules folder.

Step 4 - Make our controller run an async waterfall task

Let's get doing some async stuff!

So now if you edit your mainController.js file to look like the following, I'll explain what we are doing.

mainController.js

var async = require('async');

exports.defaultEndpoint = function (req, res) {
    var amountOfCats = function (callback) {
            var amount = Math.floor((Math.random() * 10) + 1);
            if (amount) {
                callback(null, amount);
            } else {
                callback('Error cat amount failed', null);
            }
    };

    var concatData = function (amount, callback) {
        var dataToReturn = 'Phil has ' + amount.toString() + ' cats!';
        if (dataToReturn) {
            callback(null, dataToReturn);
        } else {
            callback('Data to return failed to process', null);
        }
    };

    async.waterfall([amountOfCats, concatData], function (err, result) {
        if (err) {
            return res.status(500).json({message: err});
        }
        return res.status(200).json({message: result});
    });
};

Quite a lot of code changes here!

Most notably, we've created several functions to handle various tasks.

The first task is the 'amountOfCats' function, that calculates how many cats I have. Usually this would hit a server, or some other task that needed waiting for. This is just an example of the basics of how it works so I've just used a basic function as an example.

We then have the 'concatData' function that creates a string from the amountOfCats results.

Then the 'async.waterfall' function - this is where the tasks are defined in the order that they operate. Each task calls the callback function, either with the first parameter 'err' or 'result'. If there is an error in the chain, the waterfall will break and return the error - it will stop process. If it is successful, it will pass the results to the next function in the chain. It's a really nice way to handle chained called!

We can see that the order of calls has been defined in the async.waterfall function, in the array.

The first function called is 'amountOfCats':

var amountOfCats = function (callback) {
        var amount = Math.floor((Math.random() * 10) + 1);
        if (amount) {
            callback(null, amount);
        } else {
            callback('Error cat amount failed', null);
        }
};

It's a basic function that generates a random number between 1 and 10 and then rounds it. Then there's a basic check to see if amount exists, which is then returned as the success parameter of the callback function. If it doesn't exist, then the error parameter of the callback function is invoked and the waterfall will break, returning the error message supplied.

As previously mentioned - async waterfall is most useful for functions that require some kind of processing/response callback. E.g. you could make a request to an API and wait for a response, check if the response exists then return it as the result - as this is a basic tutorial we aren't going that in depth yet!

var concatData = function (amount, callback) {
    var dataToReturn = 'Phil has ' + amount.toString() + ' cats!';
    if (dataToReturn) {
        callback(null, dataToReturn);
    } else {
        callback('Data to return failed to process', null);
    }
};

The second function called simply takes the amount parameter supplied from the previous call and concats it into a string, which is then returned if successful.

async.waterfall([amountOfCats, concatData], function (err, result) {
    if (err) {
        return res.status(500).json({message: err});
    }
    return res.status(200).json({message: result});
});

If everything is successful, the waterfall will reach result, respond with a 200 and the message - else if there are any errors thrown at any point, it will return a 500 with the message supplied.

Step 5 - Run it!

So the code should all be good. Let's run it.

Use:

node app.js

Enter your browser at http://localhost:3000

Now you should see how many cats I have. P.S. I like cats.

Summary

We have covered the basics of creating controllers, breaking up our code and using async operations with the Async NPM module.

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

Find the previous tutorial part here.

And the code on Github here.

Phil Hudson

Read more posts by this author.