At Settled we love Node.js

Top tips for Node.js

Here at Settled Tech we chose to build our core customer platform, the Settled Hub, with Node.js because its simplicity allows us to respond quickly to customer feedback.

We’d like to share some best practices for Node.js. As a team, we prize simplicity, clarity and efficiency in our code, above cleverness, newness or ascribing to particular principles. So all of the following aim to make our product more robust and more efficient and to make our lives easier both as individual devs and as a team.

  1. Ensure that code runs from within its directory

Trying to run a node app from outside its execution directory will break the file path. To ensure that code within index.js runs in the correct context, we include the following line at the start of index.js:

process.chdir(__dirname);

2. Use Yarn for package download

We find that Yarn offers a faster, more dependable way of installing packages because:

  • It installs packages in parallel, rather than in series like NPM and bower. This makes installation faster and means a single request failure won’t disrupt an entire install
  • It creates cached versions, meaning that previously installed packages are available offline
  • It uses a flat dependency structure which avoids the problems associated with duplication and (in Windows) long file paths that arise with nested structures

3. Include scripts in package.json

Including scripts, like the following, makes code easier to understand and simpler to execute — by running npm run [SCRIPT NAME] in the command line.

"scripts": {
"postinstall": "bower install && grunt build",
"start": "node app/index.js",
"test": "mocha app/tests/*.js app/tests/**/*.js"
}

For example,

  • the postinstall command builds assets after npm install. It’s not rocket science, but it just makes everything plain and simple and upfront
  • the start script allows us to run node app/index.js with all the relevant dependencies from node_modules/.bin, so you don’t have to install NPM modules globally
  • the test script allows us to run tests without having to dig around finding out which module we’ve used and what are the file paths

Following the same principle, any custom script enables us to group together important and potentially verbose commands to perform specific functions.

4. Include a Level attribute for environments

We use a config file for each of our development, staging and production environments to define variables such as urls and authorisation details for third party services that might differ between each.

We find it useful to define a level attribute as follows:

"env": {
"level": 3,
"id": "development"
}

This helps to simplify logic that determines configuration and make it more readable, eg:

if (config.env.level === 3) {
spdy.createServer({
key: fs.readFileSync('./keys/server.key'),
cert: fs.readFileSync('./keys/server.crt'),
}, app)
.listen(port, (err) => { if (err) { throw new Error(err); }
console.log('Listening on port: ', port);
});
}

5. Use async/await to resolve/reject Promises

Until recently we used Promises to handle all our asynchronous code. But since the release of Node 8, we have been using Javascript’s async/await feature to simplify the way we resolve/reject Promises.

This is primarily related to readability, particularly in cases with nested Promises. So the following example…

const getPropertyValuation = () => {
return getAddress()
.then(address => {
...
return getPropertyDetails(address)
.then(propertyDetails => {
...
return getValuation(propertyDetails);
})
})
}

… can be rewritten with async/await as:

const getPropertyValuation = async () => {
const address = await getAddress();
const propertyDetails = await getPropertyDetails(address);

return getValuation(propertyDetails);
}

6. Use an NPM module to make API calls

We prefer to use modules like node-fetch, request or superagent. All of these enable you to build a request URL by using an easy-to-use object, in which you can include things like query strings and headers, as follows:

const options = { 
method: 'GET',
uri: 'https://sampleapi.com',
headers: {
'User-Agent': 'Request-Promise',
'Authorization': 'Basic QWxhZGRpbjpPcGVuU2VzYW1l'
},
qs: {
limit: 10,
skip: 20,
sort: 'asc'
}
}

Modules like node-fetch or request-promise-native that come with built-in Promise functionality make handling responses clear and simple, with none of the complications that arise from asynchronicity:

const request = require('request-promise-native');
const getExtractFromWikipedia = (title) => {
const options = {
method: 'GET',
uri: 'https://en.wikipedia.org/w/api.php',
qs: {
titles: title,
action: 'query',
format: 'json',
prop: 'extracts',
exintro: true
},
json: true
}
  return request(options)
.then((response) => handleReponse(response))
.catch((err) => console.log(err))
}
getExtractFromWikipedia('Miles Davis')
.then((text) => console.log(text))

7. Handle logic for routes in services

At Settled we don’t subscribe to any particular MVC best-practice — like Fat Models Skinny Controllers or Skinny Models, Skinny Controllers.

However, in an effort to keep our code readable we do try to limit our routes to handling their inputs and outputs. With this in mind we prefer to handle any more lengthy logic — for processing data or making API calls, for example — in a service that we require in the route.

Not only does using services in this way keep our code more readable, it means their code is reusable.

8. Handle user authentication in middleware

Like most Node.js users, we employ Express.js to organise our MVC architecture. Its middleware feature provides a simple way to configure and make checks on an HTTP request on its way to the server.

As well as using some of the useful NPM modules that help configure a request, we have written our own middleware module that authenticates a user with every request to the server.

This we require and use in index.js in the usual way:

const authentication = require('./middleware/authentication');
app.use(authentication);

The beauty of this module is that it not only authenticates the user, but also retrieves a base set of user data which is then available on every page.

9. Build page models from an Environment model

Javascript’s extends keyword allows you to create a class that is the child of another. This comes in handy when building page models that share commonly used data.

At Settled we create an Environment model that contains setup information and user data as follows:

...
module.exports = class EnvironmentModel {

constructor (user, cookies) {
// General
this.appVersion = …;
this.environmentLevel = …;
this.environment = …;
this.settledHomePage = …;
    // User data
if (user) {
this.userId = …;
this.userFullName = …;
this.userEmail = …;
this.userPrimaryRole = …;
}
}
};

Creating a page model as a child of the Environment model with the extends keyword means all this data is available in the new model:

const EnvironmentModel = require('./environmentModel.js');
module.exports = class FeedbackModel extends EnvironmentModel {
constructor (user, cookies, pageName, thread, feedbackId) {
super (user, cookies);
this.pageName = pageName;
this.feedbackId = feedbackId;
this.thread = thread;
}
};

Note that the super keyword is used here to call the Environment model’s constructor.