Node Anti-Patterns and Bad Practices

193
NODE.JS ANTI-PATTERNS and bad practices

Transcript of Node Anti-Patterns and Bad Practices

Page 1: Node Anti-Patterns and Bad Practices

NODE.JSANTI-PATTERNS

and bad practices

Page 2: Node Anti-Patterns and Bad Practices

ADOPTION OF NODE.JS KEEPS GROWING

Page 3: Node Anti-Patterns and Bad Practices

CHAMPIONSWalmart, eBay, PayPal, Intuit, Netflix, LinkedIn,

Microsoft, Uber, Yahoo ...

Page 4: Node Anti-Patterns and Bad Practices

JAVA → NODE.JS.NET → NODE.JS

... → NODE.JS

Page 5: Node Anti-Patterns and Bad Practices

The clash of paradigms leads to anti-patterns

Page 6: Node Anti-Patterns and Bad Practices

IGOREngineer @ YLD

Page 7: Node Anti-Patterns and Bad Practices

PEDROCTO @ YLD

Page 8: Node Anti-Patterns and Bad Practices

YLD does Node.js consulting

Page 9: Node Anti-Patterns and Bad Practices

WHERE DO THESE ANTI-PATTERNSCOME FROM?

Page 10: Node Anti-Patterns and Bad Practices

NODE.JS ANTI-PATTERNS AND BADPRACTISES

The opinionated and incomplete guide

Page 11: Node Anti-Patterns and Bad Practices

YOUR MILEAGE MAYVARY

Page 12: Node Anti-Patterns and Bad Practices

MEET JANE

Page 13: Node Anti-Patterns and Bad Practices

JANEExperienced Java developer in a big enterpriseLimited experience with JavaScript

Page 14: Node Anti-Patterns and Bad Practices

JANE'S QUESTcreate a Node.js-based prototype ofan API service for a new mobile app

Page 15: Node Anti-Patterns and Bad Practices

LET'S TRY THIS JAVASCRIPT ON THESERVER THING...

Page 16: Node Anti-Patterns and Bad Practices

function getTask(jobName, callback) { redisSlave.hmget('job:'+jobName, 'bTTG', 'beDestOf', function (err, replies) { if (err) logError(err); var bTTG = replies[0]; var beDestOf = replies[1]; redisCluster.blpop('ready:'+beDestOf, 10, function (err, task) { if (err) logError(err); if (task !== null && task.length) { var taskName = task[1]; redisCluster.hdel('t:'+taskName, 'shsh', 'iir', 'vir', function (err) { if (err) logError(err); redisSlave.hget('job:'+beDestOf, 'iterations', function (err, iterations) { if (err) logError(err); redisCluster.hincrby('t:'+taskName, 'il', iterations, function (err) { if (err) logError(err); redisCluster.hmget('t:'+taskName, 'i', 's', function (err, solution) { if (err) logError(err); callback(null, solution[0], solution[1]); }); }); }); }); } else { deactivateJob(jobName); } }); });}

Page 17: Node Anti-Patterns and Bad Practices

CALLBACK HELL+ not avoiding closures

(1/22)

Page 18: Node Anti-Patterns and Bad Practices

SYMPTOMS

Page 19: Node Anti-Patterns and Bad Practices

function getTask(jobName, callback) { redisSlave.hmget('job:'+jobName, 'bTTG', 'beDestOf', function (err, replies) { if (err) logError(err); var bTTG = replies[0]; var beDestOf = replies[1]; redisCluster.blpop('ready:'+beDestOf, 10, function (err, task) { if (err) logError(err); if (task !== null && task.length) { var taskName = task[1]; redisCluster.hdel('t:'+taskName, 'shsh', 'iir', 'vir', function (err) { if (err) logError(err); redisSlave.hget('job:'+beDestOf, 'iterations', function (err, iterations) { if (err) logError(err); redisCluster.hincrby('t:'+taskName, 'il', iterations, function (err) { if (err) logError(err); redisCluster.hmget('t:'+taskName, 'i', 's', function (err, solution) { if (err) logError(err); callback(null, solution[0], solution[1]); }); }); }); }); } else { deactivateJob(jobName); } }); });}

Page 20: Node Anti-Patterns and Bad Practices

SOLUTIONApply several techniques

Page 21: Node Anti-Patterns and Bad Practices

EXAMPLE

Page 22: Node Anti-Patterns and Bad Practices

function getTask(jobName, callback) { redisSlave.hmget('job:'+jobName, 'bTTG', 'beDestOf', function (err, replies) { if (err) logError(err); var bTTG = replies[0]; var beDestOf = replies[1]; redisCluster.blpop('ready:'+beDestOf, 10, function (err, task) { if (err) logError(err); if (task !== null && task.length) { var taskName = task[1]; redisCluster.hdel('t:'+taskName, 'shsh', 'iir', 'vir', function (err) { if (err) logError(err); redisSlave.hget('job:'+beDestOf, 'iterations', function (err, iterations) { if (err) logError(err); redisCluster.hincrby('t:'+taskName, 'il', iterations, function (err) { if (err) logError(err); redisCluster.hmget('t:'+taskName, 'i', 's', function (err, solution) { if (err) logError(err); callback(null, solution[0], solution[1]); }); }); }); }); } else { deactivateJob(jobName); } }); });}

Page 23: Node Anti-Patterns and Bad Practices
Page 24: Node Anti-Patterns and Bad Practices
Page 25: Node Anti-Patterns and Bad Practices

function getTask(jobName, callback) { redisSlave.hmget('job:'+jobName, 'bTTG', 'beDestOf', function (err, replies) { if (err) return callback(err); var bTTG = replies[0]; var beDestOf = replies[1]; redisCluster.blpop('ready:'+beDestOf, 10, function (err, task) { if (err) return callback(err); if (task !== null && task.length) { var taskName = task[1]; redisCluster.hdel('t:'+taskName, 'shsh', 'iir', 'vir', function (err) { if (err) return callback(err); redisSlave.hget('job:'+beDestOf, 'iterations', function (err, iterations) { if (err) return callback(err); redisCluster.hincrby('t:'+taskName, 'il', iterations, function (err) { if (err) return callback(err); redisCluster.hmget('t:'+taskName, 'i', 's', function (err, solution) { if (err) return callback(err); callback(null, solution[0], solution[1]); }); }); }); }); } else { deactivateJob(jobName, callback); } }); });}

Page 26: Node Anti-Patterns and Bad Practices

function getTask(jobName, callback) { redisSlave.hmget('job:'+jobName, 'bTTG', 'beDestOf', function gotJobAttributes(err, replies) { if (err) return callback(err); var bTTG = replies[0]; var beDestOf = replies[1]; redisCluster.blpop('ready:'+beDestOf, 10, function poppedReady(err, task) { if (err) return callback(err); if (task !== null && task.length) { var taskName = task[1]; redisCluster.hdel('t:'+taskName, 'shsh', 'iir', 'vir', function deletedTaskAttrs(err) { if (err) return callback(err); redisSlave.hget('job:'+beDestOf, 'iterations', function gotIterations(err, iterations) { if (err) return callback(err); redisCluster.hincrby('t:'+taskName, 'il', iterations, function incrementedIterations(err) { if (err) return callback(err); redisCluster.hmget('t:'+taskName, 'i', 's', function gotTaskSolution(err, solution) { if (err) return callback(err); callback(null, solution[0], solution[1]); }); }); }); }); } else { deactivateJob(jobName, callback); } }); });}

Page 27: Node Anti-Patterns and Bad Practices

function getTask(jobName, callback) { redisSlave.hmget('job:'+jobName, 'bTTG', 'beDestOf', gotJobAttributes);

function gotJobAttributes(err, replies) { if (err) return callback(err); var bTTG = replies[0]; var beDestOf = replies[1]; redisCluster.blpop('ready:'+beDestOf, 10, poppedReady);

function poppedReady(err, task) { if (err) return callback(err); if (task !== null && task.length) { var taskName = task[1]; redisCluster.hdel('t:'+taskName, 'shsh', 'iir', 'vir', deletedTaskAttrs); } else { deactivateJob(jobName, callback); }

function deletedTaskAttrs(err) { if (err) return callback(err); redisSlave.hget('job:'+beDestOf, 'iterations', gotIterations);

function gotIterations(err, iterations) { if (err) return callback(err); redisCluster.hincrby('t:'+taskName, 'il', iterations, incrementedIterations);

function incrementedIterations(err) { if (err) return callback(err); redisCluster.hmget('t:'+taskName, 'i', 's', gotTaskSolution);

function gotTaskSolution(err, solution) { if (err) return callback(err); callback(null, solution[0], solution[1]); } } } } } } };

Page 28: Node Anti-Patterns and Bad Practices

function getTask(jobName, callback) { redisSlave.hmget('job:'+jobName, 'bTTG', 'beDestOf', gotJobAttributes);

function gotJobAttributes(err, replies) { if (err) return callback(err); var bTTG = replies[0]; var beDestOf = replies[1]; redisCluster.blpop('ready:'+beDestOf, 10, poppedReady);

function poppedReady(err, task) { if (err) return callback(err); if (task !== null && task.length) { var taskName = task[1]; redisCluster.hdel('t:'+taskName, 'shsh', 'iir', 'vir', deletedTaskAttrs); } else { deactivateJob(jobName, callback); }

function deletedTaskAttrs(err) { if (err) return callback(err); redisSlave.hget('job:'+beDestOf, 'iterations', gotIterations); }

function gotIterations(err, iterations) { if (err) return callback(err); redisCluster.hincrby('t:'+taskName, 'il', iterations, incrementedIterations); }

function incrementedIterations(err) { if (err) return callback(err); redisCluster.hmget('t:'+taskName, 'i', 's', gotTaskSolution); } } }

function gotTaskSolution(err, solution) { if (err) return callback(err); callback(null, solution[0], solution[1]); } };

Page 29: Node Anti-Patterns and Bad Practices

function getTask(jobName, callback) { redisSlave.hmget('job:'+jobName, 'bTTG', 'beDestOf', handlingError(gotJobAttributes));

function gotJobAttributes(replies) { var bTTG = replies[0]; var beDestOf = replies[1]; redisCluster.blpop('ready:'+beDestOf, 10, handlingError(poppedReady));

function poppedReady(task) { if (task !== null && task.length) { var taskName = task[1]; redisCluster.hdel('t:'+taskName, 'shsh', 'iir', 'vir', handlingError(deletedTaskAttrs)); } else { deactivateJob(jobName, callback); }

function deletedTaskAttrs() { redisSlave.hget('job:'+beDestOf, 'iterations', handlingError(gotIterations)); }

function gotIterations(iterations) { redisCluster.hincrby('t:'+taskName, 'il', iterations, handlingError(incrementedIterations)); }

function incrementedIterations() { redisCluster.hmget('t:'+taskName, 'i', 's', handlingError(gotTaskSolution)); } } }

function gotTaskSolution(solution) { callback(null, solution[0], solution[1]); }

function handlingError(next) { return function(err) { if (err) { callback(err); } else { var args = Array.prototype.slice.call(arguments, 1); next.apply(null, args); } } } };

Page 30: Node Anti-Patterns and Bad Practices

function getTask(jobName, callback) { redisSlave.hmget('job:'+jobName, 'bTTG', 'beDestOf', handlingError(gotJobAttributes));

function popNextTask(replies) { var bTTG = replies[0]; var beDestOf = replies[1]; redisCluster.blpop('ready:'+beDestOf, 10, handlingError(deleteTaskAttributes));

function deleteTaskAttributes(task) { if (task !== null && task.length) { var taskName = task[1]; redisCluster.hdel('t:'+taskName, 'shsh', 'iir', 'vir', handlingError(getIterations)); } else { deactivateJob(jobName, callback); }

function getIterations() { redisSlave.hget('job:'+beDestOf, 'iterations', handlingError(incrementIterations)); }

function incrementIterations(iterations) { redisCluster.hincrby('t:'+taskName, 'il', iterations, handlingError(getTaskSolution)); }

function getTaskSolution() { redisCluster.hmget('t:'+taskName, 'i', 's', handlingError(gotTaskSolution)); } } }

function gotTaskSolution(solution) { callback(null, solution[0], solution[1]); }

function handlingError(fn) { return function(err) { if (err) { callback(err); } else { var args = Array.prototype.slice.call(arguments, 1); fn.apply(null, args); } } } };

Page 31: Node Anti-Patterns and Bad Practices

ASYNC

Page 32: Node Anti-Patterns and Bad Practices

function getTask(jobName, callback) { var bTTG, beDestOf, taskName;

async.waterfall([ getJobAttributes, popNextTask, deleteTaskAttributes, getIterations, incrementIterations, getTaskSolution, getFinalTaskSolution ], callback);

function getJobAttributes(cb) { redisSlave.hmget('job:'+jobName, 'bTTG', 'beDestOf', cb); }

function popNextTask(replies, cb) { bTTG = replies[0]; beDestOf = replies[1]; redisCluster.blpop('ready:'+beDestOf, 10, cb); }

function deleteTaskAttributes(task, cb) { if (task !== null && task.length) { taskName = task[1]; redisCluster.hdel('t:'+taskName, 'shsh', 'iir', 'vir', cb); } else { deactivateJob(jobName, callback); } }

function getIterations(result, cb) { redisSlave.hget('job:'+beDestOf, 'iterations', cb); }

function incrementIterations(iterations, cb) { redisCluster.hincrby('t:'+taskName, 'il', iterations, cb); }

function getTaskSolution(result, cb) { redisCluster.hmget('t:'+taskName, 'i', 's', cb); }

function getFinalTaskSolution(solution, cb) { cb(null, solution[0], solution[1]); } };

Page 33: Node Anti-Patterns and Bad Practices

};

SOLUTIONReturn earlyName your functionsMoving functions to the outer-most scope aspossibleDon't be afraid of hoisting to make the codemore readableUse a tool like async to orchestrate callbacks

Page 34: Node Anti-Patterns and Bad Practices

USING A LONG LIST OF ARGUMENTSINSTEAD OF OPTIONS

function createUser(firstName, lastName, birthDate, address1, address2, postCode, ...) {

// ..

}

(2/22)

Page 35: Node Anti-Patterns and Bad Practices

function createUser(opts) {

var firstName = opts.firstName;

var lastName = opts.lastName;

// ..

var otherValue = opts.otherValue || defaultValue;

// ..

}

Page 36: Node Anti-Patterns and Bad Practices

use utils._extend:

var extend = require('utils')._extend;

var defaultOptions = {

attr1: 'value 1',

attr2: 'value 2',

};

module.exports = MyConstructor(opts) {

var options = extend(extend({}, defaultOptions), opts);

}

Page 37: Node Anti-Patterns and Bad Practices

use xtend:

var extend = require('xtend');

var defaultOptions = {

attr1: 'value 1',

attr2: 'value 2',

};

module.exports = MyConstructor(opts) {

var options = extend({}, defaultOptions, opts);

}

Page 38: Node Anti-Patterns and Bad Practices

function myFunction(arg1, [arg2], [arg3], [arg4]) { // ... }

Page 39: Node Anti-Patterns and Bad Practices

ABUSING VARIABLEARGUMENTS

(3/22)

Page 40: Node Anti-Patterns and Bad Practices

PROBLEMSHard to make it work generallyError-prone

Page 41: Node Anti-Patterns and Bad Practices

fs.readFile = function(path, options, callback_) { var callback = maybeCallback(arguments[arguments.length - 1]);

if (typeof options === 'function' || !options) { options = { encoding: null, flag: 'r' }; } else if (typeof options === 'string') { options = { encoding: options, flag: 'r' }; } else if (!options) { options = { encoding: null, flag: 'r' }; } else if (typeof options !== 'object') { throw new TypeError('Bad arguments'); }

var encoding = options.encoding; assertEncoding(encoding); // ...

Page 42: Node Anti-Patterns and Bad Practices

POOR USE OF MODULARITY(4/22)

Page 43: Node Anti-Patterns and Bad Practices

Files with > 200 LoCLots of scattered functionsLow cohesionNo reuseTesting is hard

Page 44: Node Anti-Patterns and Bad Practices

All the handlers for a given resource inside thesame moduleModules that have loosely related functionsinside it because it's the only place thesefunctions are being used.

Page 45: Node Anti-Patterns and Bad Practices

modules are cheapexpose a documented interfacetry to keep modules under 200 LoC

Page 46: Node Anti-Patterns and Bad Practices

OVERUSE OF CLASSES FOR MODELLING(5/22)

var MOD = require('MOD'); var config = new MOD.Config({ opt: 'foobar' }); var client = new MOD.Thing.Client(config); var actor = new MOD.Thing.Actor(actorOpts); client.registerActor(actor)

Page 47: Node Anti-Patterns and Bad Practices

var MOD = require('MOD'); var config = new MOD.Config({ opt: 'foobar' }); var client = new MOD.Thing.Client(config); var actor = new MOD.Thing.Actor(actorOpts); client.registerActor(actor)

vs

var Client = require('MODClient'); var client = Client({ opt: 'foobar', actor: actorOpts });

Page 48: Node Anti-Patterns and Bad Practices

module.exports = Counter;

function Counter() { this._counter = 0; }

Counter.prototype.increment = function() { this._counter += 1; };

Counter.prototype.get = function() { return this._counter; };

Page 49: Node Anti-Patterns and Bad Practices

module.exports = function createCounter(options) {

var counter = 0;

function increment() { counter += 1; }

function get() { return counter; }

return { increment: increment, get: get, }; }

Page 50: Node Anti-Patterns and Bad Practices

LET'S TRY THIS NODE.JS THING...

Page 51: Node Anti-Patterns and Bad Practices

doThis(function(err1, result1) { doThat(result1.someAttribute, function(err2, result2) { if (err2) { ...

} else { ... } }

Page 52: Node Anti-Patterns and Bad Practices

IGNORING CALLBACKERRORS

(6/22)

Page 53: Node Anti-Patterns and Bad Practices

doThis(function(err1, result1) { doThat(result1.someAttribute, function(err2, result2) { if (err2) { ...

} else { ... } }

Page 54: Node Anti-Patterns and Bad Practices

SOLUTIONS

Page 55: Node Anti-Patterns and Bad Practices

USE A LINTERlike ESLint and enable the rule

http://eslint.org/docs/rules/handle-callback-err

Page 56: Node Anti-Patterns and Bad Practices

USE ASYNC OR SIMILARvar async = require('async'); async.waterfall([ doThis, doThat, ], done);

function doThis(cb) { // ...}

function doThat(result, cb) { // ...}

function done(err) { // you still have to handle this error! }

Page 57: Node Anti-Patterns and Bad Practices

USE PROMISESdoThis() .then(doThat). .catch(handleError);

function handleError(err) { // .. handle error }

Page 58: Node Anti-Patterns and Bad Practices

THE KITCHEN-SINK MODULE(7/22)

Page 59: Node Anti-Patterns and Bad Practices

var normalizeRequestOptions = function(options) { /* ... */ };var isBinaryBuffer = function(buffer) { /* ... */ }; var mergeChunks = function(chunks) { /* ... */ }; var overrideRequests = function(newRequest) { /* ... */ }; var restoreOverriddenRequests = function() { /* ... */ }; function stringifyRequest(options, body) { /* ... */ } function isContentEncoded(headers) { /* ... */ }function isJSONContent(headers) { /* ... */ } var headersFieldNamesToLowerCase = function(headers) { /* ... */var headersFieldsArrayToLowerCase = function (headers) { /* ... */var deleteHeadersField = function(headers, fieldNameToDelete) function percentDecode (str) { /* ... */ } function percentEncode(str) { /* ... */ } function matchStringOrRegexp(target, pattern) { /* ... */ } function formatQueryValue(key, value, options) { /* ... */ } function isStream(obj) { /* ... */ }

exports.normalizeRequestOptions = normalizeRequestOptions; exports.isBinaryBuffer = isBinaryBuffer; exports.mergeChunks = mergeChunks; exports.overrideRequests = overrideRequests; exports.restoreOverriddenRequests = restoreOverriddenRequests;exports.stringifyRequest = stringifyRequest; exports.isContentEncoded = isContentEncoded; exports.isJSONContent = isJSONContent; exports.headersFieldNamesToLowerCase = headersFieldNamesToLowerCase;exports.headersFieldsArrayToLowerCase = headersFieldsArrayToLowerCase;

Page 60: Node Anti-Patterns and Bad Practices

exports.headersFieldsArrayToLowerCase = headersFieldsArrayToLowerCase;

exports.deleteHeadersField = deleteHeadersField; exports.percentEncode = percentEncode;

https://github.com/pgte/nock/blob/master/lib/common.js

Page 61: Node Anti-Patterns and Bad Practices

1. Embrace modules2. Enforce SRP3. Externalise modules4. Individualised packaging

Page 62: Node Anti-Patterns and Bad Practices

initialization:global.App = ...

from any file:App.Models.Person.get(id);

Page 63: Node Anti-Patterns and Bad Practices

PLACING VALUES INGLOBAL OBJECTS

(8/22)

Page 64: Node Anti-Patterns and Bad Practices

SYMPTOMSAdding properties to any of these:

processglobalGLOBALrootthis on the global scopeany other global reference, e.g. Buffer orconsole

Page 65: Node Anti-Patterns and Bad Practices

EXAMPLESglobal.utilityFunction = function() { /*...*/ };

// or ...

global.maxFoosticles = 10;

Page 66: Node Anti-Patterns and Bad Practices

PROBLEMDependencies become implicit instead ofexplicit.Makes the code harder to reason about for anewcomer

Page 67: Node Anti-Patterns and Bad Practices

SOLUTIONLeverage the module cache

Page 68: Node Anti-Patterns and Bad Practices

EXAMPLECreate a file module:

exports.maxFoosticles = 10;

Require this file module in other filesvar config = require('./config'); config.maxFoosticles // => 10

Page 69: Node Anti-Patterns and Bad Practices

EXAMPLE:config.js:

module.exports = { couchdb: { baseUrl: "https://my.couchdb.url:4632" || process.env.COUCHDB_URL }, mailchimp: { // ... }}

Page 70: Node Anti-Patterns and Bad Practices

EXAMPLE 2models/people.js

module.exports = new PeopleModel();

client:var People = require('./models/people');People.find(...);

Page 71: Node Anti-Patterns and Bad Practices

EXCEPTIONSTesting framework...?

Page 72: Node Anti-Patterns and Bad Practices

var exec = require('child_process').execSync;

module.exports = function pay(req, reply) { var fraudCheck = exec('fraud_check', JSON.stringify(req.payload)); // ...};

Page 73: Node Anti-Patterns and Bad Practices

SYNCHRONOUS EXECUTION AFTERINITIALISATION

module.exports = function getAttachment(req, reply) { db.getAttachment(req.params.id, loadAttachment); function loadAttachment(err, path) { if (err) return reply(err); reply(fs.readFileSync(path, { encoding: 'utf-8' })); } };

(9/22)

Page 74: Node Anti-Patterns and Bad Practices

SYMPTOMSHigher request latencyPerformance decays quickly when under load

Page 75: Node Anti-Patterns and Bad Practices

fs.readFileSyncfs.accessSyncfs.changeModSyncfs.chownSyncfs.closeSyncfs.existsSync...

Page 76: Node Anti-Patterns and Bad Practices

Asynchronous initialisation

var cache = require('./cache'); cache.warmup(function(err) { if (err) throw err; var server = require('./server'); server.start(); });

Page 77: Node Anti-Patterns and Bad Practices

mongoose.find().stream().pipe(transform).pipe(res);

Page 78: Node Anti-Patterns and Bad Practices

DANGLING SOURCESTREAM

(10/22)

Page 79: Node Anti-Patterns and Bad Practices

SYMPTOMSWhen a stream throws an error or closes whilepiping, streams are not properly disposed and

resources leak.

Page 80: Node Anti-Patterns and Bad Practices

SOLUTIONlisten for error and close events on everystream and cleanupor use the pump package instead of the nativestream.pipe()

Page 81: Node Anti-Patterns and Bad Practices

WRONG:mongoose.find().stream().pipe(transform).pipe(res);

Page 82: Node Anti-Patterns and Bad Practices
Page 83: Node Anti-Patterns and Bad Practices
Page 84: Node Anti-Patterns and Bad Practices
Page 85: Node Anti-Patterns and Bad Practices
Page 86: Node Anti-Patterns and Bad Practices
Page 87: Node Anti-Patterns and Bad Practices

BETTER:var stream = mongoose.find().stream(); var transform = ...;

var closed = false; stream.once('close', function() { closed = true;});

transform.on('error', function(err) { if (! closed) stream.destroy(); });

transform.on('close', function(err) { if (! closed) stream.destroy(); });

// ...and the same thing for transform <-> res

stream.pipe(transform).pipe(res);

Page 88: Node Anti-Patterns and Bad Practices

EVEN BETTER:var pump = require('pump');

pump(mongoose.find().stream(), transform, res);

Page 89: Node Anti-Patterns and Bad Practices

var baz = require('../../../foo/bar/baz');

Page 90: Node Anti-Patterns and Bad Practices

CHANGING THE WAYrequire()

WORKSvar baz = require('/foo/bar/baz');

(11/22)

Page 91: Node Anti-Patterns and Bad Practices

Setting NODE_PATHUsing a module that requires in a different way.e.g.

'rootpath''require-root''app-root-path''root-require'

Page 92: Node Anti-Patterns and Bad Practices

$ tree . ├── lib │ ├── bar │ │ └── bar.js │ ├── foo │ │ └── foo.js │ └── index.js├── node_modules│ └── ... ├── package.json└── test └── suite.js

Page 93: Node Anti-Patterns and Bad Practices
Page 94: Node Anti-Patterns and Bad Practices

THE MONOLITHICAPPLICATION

(12/22)

Page 95: Node Anti-Patterns and Bad Practices

NODE IS GREAT FOR PROTOTYPINGBut this may become a trap

Page 96: Node Anti-Patterns and Bad Practices

EXAMPLESViews and API on the same code baseServices that do a lot of disjoint things

Page 97: Node Anti-Patterns and Bad Practices

SYMPTOMS

Page 98: Node Anti-Patterns and Bad Practices

POOR TEST COVERAGE

Page 99: Node Anti-Patterns and Bad Practices

BRITTLE IN SOME PARTS

Page 100: Node Anti-Patterns and Bad Practices

NOT MUCH ELBOW ROOM

Page 101: Node Anti-Patterns and Bad Practices

LONG DELIVERY CYCLESand high error rate

Page 102: Node Anti-Patterns and Bad Practices

LONG TIME OF CODE ONBOARDING ANDHAND-HOLDING

Page 103: Node Anti-Patterns and Bad Practices

HOW WE GET THERE

Page 104: Node Anti-Patterns and Bad Practices

EXAMPLE

Page 105: Node Anti-Patterns and Bad Practices
Page 106: Node Anti-Patterns and Bad Practices
Page 107: Node Anti-Patterns and Bad Practices
Page 108: Node Anti-Patterns and Bad Practices
Page 109: Node Anti-Patterns and Bad Practices
Page 110: Node Anti-Patterns and Bad Practices
Page 111: Node Anti-Patterns and Bad Practices
Page 112: Node Anti-Patterns and Bad Practices
Page 113: Node Anti-Patterns and Bad Practices

SOLUTIONS

Page 114: Node Anti-Patterns and Bad Practices

SEPARATE VIEWS FROM APIEmbrace Cross-origin resource sharing.

Page 115: Node Anti-Patterns and Bad Practices

NODE IS GREAT AT NETWORKING

Page 116: Node Anti-Patterns and Bad Practices

SLOWLY MIGRATEKEEP THE MONOLITH RUNNING

but develop new or updated features into separatesmaller services

Page 117: Node Anti-Patterns and Bad Practices

TURN A MACRO-SERVICE INTO A SET OFMICRO-SERVICES

Page 118: Node Anti-Patterns and Bad Practices

CHALLENGESVersioning, Testing, Shared Asset Management,

Deploying, Service Lookup

Page 119: Node Anti-Patterns and Bad Practices

TESTING

Page 120: Node Anti-Patterns and Bad Practices

LITTLE OR NO AUTOMATED TESTS$ tree . ├── lib │ ├── bar.js │ ├── foo.js │ └── index.js├── node_modules│ └── ... └── package.json

(13/22)

Page 121: Node Anti-Patterns and Bad Practices

Project on-boarding takes a long timeApp is brittle, needs fixing in production all thetimeDevelopers are reluctant to make changesQA process doesn't seem strict enoughQA cycle takes too long

Page 122: Node Anti-Patterns and Bad Practices

CAUSESLack of experience writing testsNo testing culture

Weak quality cultureManagement doesn't value tests"Wasting" time in automated tests isforbidden

Page 123: Node Anti-Patterns and Bad Practices

Start with tests — TDDMeasure test coverage, aim for 100%Start with regression tests in existing monoliths

Page 124: Node Anti-Patterns and Bad Practices

test('do something', function(t) { var MyClass = require('..'); a = MyClass(); a.doSomething(); t.equal(a._privateThing, 'some value'); t.end(); });

Page 125: Node Anti-Patterns and Bad Practices

TESTING AT THEWRONG LEVEL

(14/22)

Page 126: Node Anti-Patterns and Bad Practices

SYMPTOMS

Page 127: Node Anti-Patterns and Bad Practices

UNIT TESTS THAT REACH INTO THEBOWELS OF A MODULE

Page 128: Node Anti-Patterns and Bad Practices

TESTING THE IMPLEMENTATION, NOTTHE INTERFACE

Page 129: Node Anti-Patterns and Bad Practices

FILE MODULES EXPOSING EXTRADETAILS

Page 130: Node Anti-Patterns and Bad Practices

CHANGING IMPLEMENTATION DETAILSoften requires updating the tests

Page 131: Node Anti-Patterns and Bad Practices

CAUSES

Page 132: Node Anti-Patterns and Bad Practices

POOR USE OF MODULARITY

Page 133: Node Anti-Patterns and Bad Practices

POOR SEPARATION OF CONCERNS

Page 134: Node Anti-Patterns and Bad Practices

EXAMPLETesting a side effect:

test('do something', function(t) { var MyClass = require('../'); a = MyClass(); a.doSomething();

test.equal(a._privateThing, 'some value');

t.end(); });

Page 135: Node Anti-Patterns and Bad Practices

EXAMPLEInvoking a private API:

test('do something', function(t) { var MyClass = require('../'); a = MyClass();

test.equal(a._doSomethingPrivate(), 'some value');;

t.end(); });

Page 136: Node Anti-Patterns and Bad Practices

SOLUTIONS

Page 137: Node Anti-Patterns and Bad Practices

TEST AT THE INTERFACE LEVELAll that the tests should require is

var mymodule = require('..')

Page 138: Node Anti-Patterns and Bad Practices

Test the behaviour not the implementationDon't conflate concerns on the same moduleExternalise: Make good use of NPM

Page 139: Node Anti-Patterns and Bad Practices

FACILITATE TESTINGBy overriding default options

options.timeout = muchShorterValue;

Page 140: Node Anti-Patterns and Bad Practices

USE MOCKS, SPIES OR DEPENDENCY INJECTION FORTHIRD-PARTY PACKAGES

as a last resource

and as long as you don't spy on internal stuff

Page 141: Node Anti-Patterns and Bad Practices

function mockClient(code, path, extra) { return function(debug, error) { extra = extra || {}; var opts = _.extend(extra, { url: helpers.couch + path, log: debug, request: function(req, cb) { if(error) { return cb(error); }

if(code === 500) { cb(new Error('omg connection failed')); } else { cb(null, { statusCode: code, headers: {} }, req); } } });

return Client(opts); }; }

Page 142: Node Anti-Patterns and Bad Practices

FOCUS ON TESTING THE INTERFACE

Page 143: Node Anti-Patterns and Bad Practices

COLLABORATION ISHARD...

Page 144: Node Anti-Patterns and Bad Practices

DEPENDING ON GLOBALLY INSTALLED MODULES ONNPM SCRIPTS

Jane$ npm install -g lattemacchiato ... installed version 1.4 ...

... "scripts": { "test": "lattemacchiato --extra-sugar test/ }, ...

Jane$ npm test lattemacchiato: all ok!

(15/22)

Page 145: Node Anti-Patterns and Bad Practices

julia$ npm test command not found: lattemacchiato julia$ npm install -g lattemacchiato ... installed version 2.3 ...

Page 146: Node Anti-Patterns and Bad Practices

$ npm i --save-dev lattemacchiato

... "scripts": { "test": "lattemacchiato --extra-sugar test/" }, "devDependencies": { "lattemacchiato": "̂1.4" ...

$ ls node_modules/.bin lattemacchiato

Page 147: Node Anti-Patterns and Bad Practices
Page 148: Node Anti-Patterns and Bad Practices

USING GULP ORGRUNT INSTEAD OF

NPM SCRIPTS(16/22)

Page 149: Node Anti-Patterns and Bad Practices

SYMPTOMS

Page 150: Node Anti-Patterns and Bad Practices

LONG TIME SPENT ON TOOLING

Page 151: Node Anti-Patterns and Bad Practices

HARD TO CHANGE THE TASKS

Page 152: Node Anti-Patterns and Bad Practices

Start by using NPM scripts to automate tasksUse the package.json "pre" and "post" scripthooksUse default config inside package.json

Then run with

$ npm run mytask

Page 153: Node Anti-Patterns and Bad Practices

TYPICAL TASKSAutomated TestsTransformationsWatchingLive-reloadingStarting service...

Page 154: Node Anti-Patterns and Bad Practices

EXAMPLE

Page 155: Node Anti-Patterns and Bad Practices

{ "name": "mytestapp", "version": "0.0.1", "config": { "reporter": "xunit" }, "scripts": { "start": "node .", "prestart": "npm run build", "test": "mocha tests/*.js --reporter $npm_package_config_reporter" "lint": "eslint", "test:watch": "watch 'npm test' .", "build": "npm run build:js && npm run build:css", "build:js": "browserify src/index.js > dist/index.js", "build:css": "stylus assets/css/index.styl > dist/index.css" }, "devDependencies": { "eslint": "2.2.0", "pre-commit": "1.1.2", "mocha": "2.4.5", "watch": "0.17.1", "stylus": "0.53.0", "browserify": "13.0.0" }, "pre-commit": [ "eslint", "test"

Page 156: Node Anti-Patterns and Bad Practices

] }

Page 157: Node Anti-Patterns and Bad Practices

CHALLENGESMake Windows-compatible scriptsKnow when to switch to gulp (instead of buildinga gulp-like system)

Page 158: Node Anti-Patterns and Bad Practices

NOT MEASURING CODE COVERAGE(17/22)

Page 159: Node Anti-Patterns and Bad Practices

ISTANBULinstrumentation: excludes: ['test', 'node_modules'] check: global: lines: 100 branches: 100 statements: 100 functions: 100

Page 160: Node Anti-Patterns and Bad Practices

"scripts": { "test": "node --harmony tests/test.js", "coverage": "node --harmony node_modules/istanbul/lib/cli.js cover tests/test.js && istanbul check-coverage" "coveralls": "cat ./coverage/lcov.info | coveralls && rm -rf ./coverage" "jshint": "jshint lib/*.js", "changelog": "changelog nock all -m > CHANGELOG.md" }, "pre-commit": [ "jshint", "coverage" ]

Page 161: Node Anti-Patterns and Bad Practices

POOR USE OF NPM(18/22)

Page 162: Node Anti-Patterns and Bad Practices

NPM is the biggest and fastest growing opensource package repoMake good use of existing open-source

Page 163: Node Anti-Patterns and Bad Practices

Ignorance of existing modulesNIH syndromeReluctance with dependency management"My needs are unique"

Page 164: Node Anti-Patterns and Bad Practices

I need a testing framework thatcomputes code coverage and sends

coverage stats to coveralls.io

\̄_(ツ)_/̄

Page 165: Node Anti-Patterns and Bad Practices

I need a testing framework

tap, mocha, lab

I need to compute code coverage

istanbul

I need to send coverage stats tocoveralls.io

coveralls

Page 166: Node Anti-Patterns and Bad Practices

DISCOVERABILITYlibraries.iogithubmailing listsIRCsocial networks

Page 167: Node Anti-Patterns and Bad Practices

LicenseRelease frequencyLast updatedOpen issuesTest coverageDocumentation quality

Page 168: Node Anti-Patterns and Bad Practices

SECURITYNode Security Project — Snyk:

https://nodesecurity.io/https://snyk.io/

Page 169: Node Anti-Patterns and Bad Practices

PERFORMANCE ISHARD...

Page 170: Node Anti-Patterns and Bad Practices

PERFORMING CPU-HEAVY WORKfunction myHandler(req, res) { var result = req.body.items.reduce(reducer); res.send(result); }

(19/22)

Page 171: Node Anti-Patterns and Bad Practices

Parsing (response from the database, responsefrom external service)Computation-heavy work (like Natural LanguageProcessing, Classification, Learning, etc.)Processing big sequences of dataMapping or a big datasetAggregating a big datasetCalculating an HMAC for a big document

Page 172: Node Anti-Patterns and Bad Practices
Page 173: Node Anti-Patterns and Bad Practices

UNLIMITED ASYNCHRONOUSITERATIONS

#performance #reliability

(20/22)

Page 174: Node Anti-Patterns and Bad Practices

SYMPTOMSHigh request latency at times

Page 175: Node Anti-Patterns and Bad Practices

REASONSThe event loop is busy leads to application hickups.

Page 176: Node Anti-Patterns and Bad Practices

EXAMPLEasync.each instead of async.eachLimitasync.map instead of async.mapLimit

Page 177: Node Anti-Patterns and Bad Practices

Example on a messaging app (adapted):

module.exports = function getConversation(req, reply) { Conversations.get(req.params.id, function(err, conversation) if (err) return reply(err);

async.map(conversation.participants, UserProfiles.get, done);

function done(err, participants) { if (err) return reply(err); else reply(...) } }); }

Page 178: Node Anti-Patterns and Bad Practices

SACRIFICE RESPONSE TIME FOR THE GREATER GOODAND LIMIT THE CONCURRENCY:

module.exports = function getConversation(req, reply) { Conversations.get(req.params.id, function(err, conversation) if (err) return reply(err);

async.mapLimit(conversation.participants, 5, UserProfiles.get, done);

function done(err, participants) { if (err) return reply(err); else reply(...) } }); }

Page 179: Node Anti-Patterns and Bad Practices

LARGE DENORMALISED DOCUMENTS{ _id: ... items: [ { itemId: ..., } ], history: [ ... ] }

(21/22)

Page 180: Node Anti-Patterns and Bad Practices

Large memory consumptionRequest latency spikes

Page 181: Node Anti-Patterns and Bad Practices

Improve the schemaStreamMinimise marshalling

Page 182: Node Anti-Patterns and Bad Practices

module.exports = function findPeople(req, reply) { People. find(req.params). limit(100). exec(callback);

function callback(err, results) { reply(err || results); } };

Page 183: Node Anti-Patterns and Bad Practices

MISSING THE OPPORTUNITY OF USINGSTREAMS

#reliability #performance #maintainability

(22/22)

Page 184: Node Anti-Patterns and Bad Practices

SYMPTOMSResponse time bubblesHigh memory consumption

Page 185: Node Anti-Patterns and Bad Practices

EXAMPLEBuffering query result set before replying

module.exports = function findPeople(req, reply) { People. find(req.params). limit(100). exec(callback);

function callback(err, results) { reply(err || results); } };

Page 186: Node Anti-Patterns and Bad Practices

Now, streaming:

module.exports = function findPeople(req, reply) { var json = JSONStream(); var peopleStream = People. find(req.params). stream();

reply(pump(peopleStream, json)); };

Page 187: Node Anti-Patterns and Bad Practices

CHALLENGESStreams APIError handling (header is sent before the body)

Page 188: Node Anti-Patterns and Bad Practices

BENEFITSSmaller TTFB (time to first byte)Less buffering -> less memory consumed ->smaller / fewer GC pauses

Page 189: Node Anti-Patterns and Bad Practices

MAIN TAKE-AWAY'SNode is fundamentally different from the other

technologies frequently used in big teams.

Adopting Node also means adopting its newerpractices.

More at blog.yld.io

Page 190: Node Anti-Patterns and Bad Practices

PEDRO TEIXEIRA@pgte — [email protected]

 

IGOR SOAREZ@igorsoarez — [email protected]

Page 191: Node Anti-Patterns and Bad Practices
Page 192: Node Anti-Patterns and Bad Practices

THANK YOU!

Page 193: Node Anti-Patterns and Bad Practices

Q&A