Node.js Code & Style Guide
There is no official document that governs the style of node.js applications. This guide is my opinionated attempt to bring you a good set of instructions that will allow you to create beautiful and consistent software. You are free to adapt to your own code style and practice, and stick to it.
This article is a personal adaptation from Felix's Node.js Style Guide, an excellent resource to derive inspiration from.
Code Editor
You are free to pick an editor of your choice. Popular choices are vim, eclipse, netbeans, webstorm. If you find Eclipse and Net-beans too CPU hogging, you can try Sublime Text or Cloud9 editors. Both editors offer excellent features for writing code, almost as good as a full featured IDE. You can easily install plug-ins for code formatting and code completion to suit your needs.
Indentation
Tab space should be set to 2 spaces. Convert tabs to spaces. You can configure your favorite editor to do this automatically for you.
Vim
Edit the .vimrc file to contain this:
set tabstop=2 shiftwidth=2 expandtab
Eclipse
Make sure you have downloaded Eclipse for JavaScript developers, it comes bundled with lots of js
goodies. Customize your preferences to use spaces for indentation.
1. Go to: Windows > Preferences > Javascript > Editor > Typing > Tabulators
2. Click on 'formatter preference page'
3. Edit:
- set tab policy as 'spaces only'
- indentation size: '2'
- tab size: '2'
4. Save this as new preferences
*Note: you have to do this for all your work spaces.
Netbeans
1. Go to Tools > Options > Editor > Formatting
2. Set 'Number of spaces per Ind..' to 2
3. Set 'Tab size' to 2.
4. Press Ok button.
Sublime Text
Install jsFormat plug-in for Sublime Text editor, you can do this easily with the package manager.
1. Go to Preferences > Package Settings > jsFormat > Settings - User
2. Edit/Update user preferences, a sample config is shared below:
{
"brace_style": "collapse",
"jslint_happy": true,
"keep_array_indentation": false,
"max_preserve_newlines": 4,
"preserve_newlines": true,
"keep_function_indentation": false,
"indent_with_tabs": false,
"ensure_newline_at_eof_on_save": true,
"space_before_line_starters": true
}
Variables
Variables exist throughout the function they have been declared in, irrespective of where they were first declared. Blocks defined by braces {}
do not start a new scope.
As corollary of this, always declare variables at the start of the function. The only exception is a for loop index variable.
for (var i = 0; i < arr.length; i++) {
...
...
}
Functions
Public functions should have a body of comments preceding them, that explains input/output parameters and any usage quirks. Add @api public
to indicate exposed interfaces.
/**
* Apply the values to a mail template and return the compiled mail object.
* @param id: ID of the email template being used
* @param tokens: list of objects to be used
* @param callback(err, mails)
* @return this - for chaining
* @api public
*/
function compileTemplate (id, tokens, callback) {
...
...
}
Default Arguments
In Node.js, functions can take variable number of arguments. It is possible to specify defaults in many ways.
One way is to check for the presence of last variable and assign it a default value if not defined.
function myFunc (x, y, z) {
if (!z) {
z = 12;
}
...
}
If you were to expose two api's viz, sendmail (options, callback)
and sendmail (callback)
where second implementation does require the options
object, you could do that like this;
function sendmail (options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
...
}
It is also possible to do this using the arguments
object.
function myFunc (x, y, z) {
if (arguments.length === 1) {
// only x is available
...
} else if (arguments.length === 2) {
// x and y are available
...
}
...
}
Note that for historical reasons arguments
is not an array, even though it looks like one. It is easy to convert it to an array though;
var args = Array.prototype.slice.call(arguments);
Argument Types
Sometimes functions would accept arguments of different types, say a function lookup (entry, callback)
could accept single entry or multiple entries as arguments.
function lookup (entry, callback) {
if (entry instanceof String)
entry = [ entry ];
// Now write code assuming entry is an array
...
}
Objects
To create a new class, define a constructor as follows;
var MyObject = function () {
};
Constructor names should always begin with uppercase. This helps differentiate constructors from regular functions.
When creating a new object of type MyObject
, use the new
keyword.
var obj = new MyObject();
assert(obj instanceof MyObject);
One common error is to omit the new
keyword when calling the constructor. This can lead to unexpected results, as this
would then have different values. Naming the constructor in uppercase is one way of addressing this. Another is to include the following check in the constructor.
var MyObject = function () {
if (!(this instanceof MyObject)) {
throw new Error ('MyObject constructor called without new');
}
...
};
Inheritance
Using util.inherits
is the preferred way for making one object inherit another. To inherit an object, call the parent constructor, and then call util.inherits
.
// To inherit Model object into States object
var util = require('util');
var Model = require('./model');
function States () {
Model.call(this, 'states');
}
util.inherits(States, Model);
Errors
Errors in node should be a subclass of the Error class. To throw a new error, create it first using new.
var err = new Error();
Additionally, you can pass an error message while constructing the error object.
var err = new Error('User does not exist');
This error can subsequently be accessed as err.message
.
Strict Mode & Namespace
Node 0.6+ supports strict mode, which prevents using undeclared variables and can be a lifesaver. In each file, let this be the first statement;
'use strict';
On the client side - not all browser will support strict mode, so it is best to limit yourself to the function form, which ensures your use strict
keyword only applies to your function.
(function() {
// your variables and code goes here
})();
Variables exist in the function they are defined in, so using the above prevents namespace pollution and your variables would not be leaked out.
Another way to prevent namespace pollution is by declaring everything inside of another variable, as its properties.
var myModule = {};
myModule.someVar = 'some value';
myModule.someFunc = function () {
...
}
Now there won't be a chance of someVar
interfering with another someVar
declared elsewhere.
Please note that everything in a module that is required into another file is private by default, so the above doesn't apply there. Unless you use module.exports
or exports
to export it, module variables will not be accessible outside.
Modules
Requiring a Module
Declare each variable on a separate line
var util = require('util');
var mysql = require('mysql');
Writing a new Module
When you require a module in a file, its content is executed. This means that any code that is not inside a function will be executed. However, this happens only once due to the presence of require cache
. You can take advantage of this feature to do one time initialization activities.
Similarly, adding global variables to the module is also safe because node will automatically create a function scope.
It's a good practice to add some minimal test code at the bottom of the module file. This helps in stand-alone testing each individual module.
/************************ Test Code ************************/
if (require.main === module) {
(function () {
// your test code goes here
...
})();
}
Organize your code - put related modules in a sub-directory, and then expose them via an index.js
. Re-factor your code if it gets too lengthy, as good practice, ensure each of your module conclude in about 200 lines of code. A small amount of time spent in this part of the code will probably save you from a lot of trouble later.
exports
Any interfaces you want to expose should be added to the exports
object. Generally, we could just overwrite exports
(or module.exports
- both are equivalent), like this;
module.exports = {
// code goes here
};
or better still,
var myModule = {
get: function () {
...
},
set: function (x) {
...
}
};
module.exports = myModule
Never declare a global variable inside module.exports
, unless you want to make something really global. Everything outside module.exports
is private, and everything specified inside module.exports
is public. The following is very dangerous practice;
module.exports = function () {
var i = 5;
// do something
};
Now i
wound not only be defined everywhere, it would be set to 5. This is going to break stuff somewhere, and must be avoided.
Loops
When looping over an array, JavaScript supports multiple forms.
C Style for Loop
var i = 0;
for (i; i < items.length; i++) {
var item = items[i];
// do something with item
}
forEach Loop
items.forEach (item, index, arr) {
// do something with item
}
Iterating over keys
var entry;
for (entry in items) {
var item = items[entry];
}
Using the form (C Style
) is preferred because it is several times faster than other examples. Use the last form (iterating over keys
) only when iterating over an Object.
Callbacks
When to use callbacks
Use callbacks if you really have to use one. If your function is not blocking in nature, i.e., if no IO operations (file reading/network IO/db query) are involved and no other callback is being called from that function, then it should not be written as a callback. For example, a function that validates its arguments or re-formats the arguments in some way is not expected to block and shouldn't be written to take a callback argument.
Following piece of code is bad practice;
function validate (args, callback) {
if (args && args.length > 4) {
callback (null, args);
} else {
callback(new Error('args not set'));
}
}
You should just do this as a simple function that returns a value.
function (validate) {
if (args && args.length > 4)
return args;
return null;
}
By convention, every callback has error as its first argument. You may notice in some of the core node.js api methods, this is not followed strictly, but that is only to conform to some historical code convention. Ensure to have the error object as the first argument in callbacks that you write. Within a callback, it is preferred to handle the success case first.
module.getUserDetails(filters, function (err, res) {
if (!err && res) {
// handle success case
...
} else {
// the failure case
...
}
});
Avoid using return
in a callback, it makes debugging difficult.
Error Handling in Callbacks
Do not use try/catch
in a callback. Event loops would make it useless. The only place where you should use try/catch
is when using JSON.parse
.
As a corollary to this, do not throw errors using throw. The right way to throw errors is to call the next callback with an error argument.
By convention, a callback function always has an error object as the first argument. This is undefined
if there are no errors.
function mycallback (err, data) {
if (!err) {
// do something
...
} else {
// deal with the error
...
}
}
Do not create closures if it is not required. This is bad practice;
function mycallback (id, callback) {
var query = 'select * from register where id=' + id;
db.query (query, function (err, res) {
if (err)
callback(err, null);
else
callback(null, res);
});
}
Instead of this, just write the above code as;
function mycallback (id, callback) {
var query = 'select * from register where id=' + id;
db.query (query, callback);
}
Closures and Loop Callbacks
Consider the following piece of code;
for (var i = 0; i < idlist.length; i++) {
var item = idlist[i];
db.query ('select * from register where id=' + id, function(err, res) {
console.log('query executed for id: ' + id);
});
}
This is not going to work as expected. Here, variable id
is accessible in the callback because a closure is created. The closure has access to the variable, but it gets the value of i
at the time it is executed. It is quite likely that the above loop will have all callbacks print the same, final value of id
(idlist[idlist.length - 1]
). This is because by the time the query finishes, the loop will have completed and the value of id
has changed to this.
There are several ways to fix this. But first, other than the fact that the above code doesn't work, please remember that closures have a cost, and we should avoid creating closures in a loop. If we didn't require to use the variable id
here, we should have written it like this to avoid closure being created;
function postQuery (err, res) {
// do something with the results of the query
}
for (var i = 0; i < idlist.length; i++) {
db.query('select * from register where id=' + idlist[i], postQuery);
}
The code above will not create closures. i
, if used inside postQuery
will be undefined.
So how do we access i
in the loop as intended earlier, there are several ways to do so.
- Create a scope, and pass
i
as an argument.
for (var i = 0; i < idlist.length; i++) {
var id = idlist[i];
(function () {
db.query('select * from register where id=' + id, function (err, res) {
console.log('query executed for id: ' + id);
});
})(id);
}
Variables have function scope in JavaScript. Passing id
as an argument means that id
that is accessible to db.query
is a different variable from the id
in the loop.
- Use a variable that is not modified in loop iterations;
for (var i = 0; i < idlist.length; i++) {
var id = idlist[i];
db.query('select * from register where id=' + id, function (err, res) {
// use idlist here directly, instead of using id.
});
}
Waiting for the last callback
Sometimes you want to wait for the the last callback to execute and then proceed to do something further. The technique here is to use a counter and set it to the number of loop iterations. Each callback should decrement the counter by 1, and then check if the result is zero. The last counter will always get a zero result and can trigger the post processing.
var results = {};
var flag = idlist.length;
function check () {
if (--flag === 0) {
callback(null, results);
}
}
for (var i = 0; i < idlist.length; i++) {
var id = idlist[i];
(function () {
db.query('select * from register where id=' + id, function (err, res) {
results[id] = res;
check();
});
})(id);
}
Assigning Default Values
Sometimes, a default value can simply be assigned with a logical or
operator.
var limit = req.query.limit || 30;
This is more concise and readable than using an if
statement.
Event Emitter
Node.js is asynchronous, and there are two ways in node to return
results back to the caller.
- via callbacks
- via event emitters
Event emitters are more advanced features than callbacks, and a very powerful programming paradigm in node.js. They are more like the event loop that runs on the inside.
server.on('connection', function (stream) {
console.log('someone connected!');
});
Logging
Use util.log
to log output. Never use console.log
. In case of errors, use util.error
.
I have explained working with git
on the other article Effective Git Workflow, which discusses best practices for effective git usage.
Alright then, I hope to have covered basic concepts for you to get started with node.js. Let me know if you have any specific questions or doubts.
Happy Coding!
\m/