NODE.JS ANTI-PATTERNS and bad practices ADOPTION OF NODE.JS KEEPS - - PowerPoint PPT Presentation

node js anti patterns
SMART_READER_LITE
LIVE PREVIEW

NODE.JS ANTI-PATTERNS and bad practices ADOPTION OF NODE.JS KEEPS - - PowerPoint PPT Presentation

NODE.JS ANTI-PATTERNS and bad practices ADOPTION OF NODE.JS KEEPS GROWING CHAMPIONS Walmart, eBay, PayPal, Intuit, Netflix, LinkedIn, Microsoft, Uber, Yahoo ... JAVA NODE.JS .NET NODE.JS ... NODE.JS The clash of paradigms leads to


slide-1
SLIDE 1

NODE.JS ANTI-PATTERNS

and bad practices

slide-2
SLIDE 2

ADOPTION OF NODE.JS KEEPS GROWING

slide-3
SLIDE 3

CHAMPIONS

Walmart, eBay, PayPal, Intuit, Netflix, LinkedIn, Microsoft, Uber, Yahoo ...

slide-4
SLIDE 4

JAVA → NODE.JS

.NET → NODE.JS

... → NODE.JS

slide-5
SLIDE 5

The clash of paradigms leads to anti-patterns

slide-6
SLIDE 6

IGOR

Engineer @ YLD

slide-7
SLIDE 7

PEDRO

CTO @ YLD

slide-8
SLIDE 8

YLD does Node.js consulting

slide-9
SLIDE 9

WHERE DO THESE ANTI-PATTERNS COME FROM?

slide-10
SLIDE 10

NODE.JS ANTI-PATTERNS AND BAD PRACTISES

The opinionated and incomplete guide

slide-11
SLIDE 11

YOUR MILEAGE MAY VARY

slide-12
SLIDE 12

MEET JANE

slide-13
SLIDE 13

JANE

Experienced Java developer in a big enterprise Limited experience with JavaScript

slide-14
SLIDE 14

JANE'S QUEST

create a Node.js-based prototype of an API service for a new mobile app

slide-15
SLIDE 15

LET'S TRY THIS JAVASCRIPT ON THE SERVER THING...

slide-16
SLIDE 16 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); } }); }); }
slide-17
SLIDE 17

CALLBACK HELL

+ not avoiding closures (1/22)

slide-18
SLIDE 18

SYMPTOMS

slide-19
SLIDE 19 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); } }); }); }
slide-20
SLIDE 20

SOLUTION

Apply several techniques

slide-21
SLIDE 21

EXAMPLE

slide-22
SLIDE 22 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); } }); }); }
slide-23
SLIDE 23
slide-24
SLIDE 24
slide-25
SLIDE 25 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); } }); }); }
slide-26
SLIDE 26 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); } }); }); }
slide-27
SLIDE 27 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]); } } } } } } };
slide-28
SLIDE 28 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]); } };
slide-29
SLIDE 29 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); } } } };
slide-30
SLIDE 30 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); } } } };
slide-31
SLIDE 31

ASYNC

slide-32
SLIDE 32 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]); } };
slide-33
SLIDE 33 };

SOLUTION

Return early Name your functions Moving functions to the outer-most scope as possible Don't be afraid of hoisting to make the code more readable Use a tool like async to orchestrate callbacks

slide-34
SLIDE 34

USING A LONG LIST OF ARGUMENTS INSTEAD OF OPTIONS

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

(2/22)

slide-35
SLIDE 35 function createUser(opts) { var firstName = opts.firstName; var lastName = opts.lastName; // .. var otherValue = opts.otherValue || defaultValue; // .. }
slide-36
SLIDE 36

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); }
slide-37
SLIDE 37

use xtend:

var extend = require('xtend'); var defaultOptions = { attr1: 'value 1', attr2: 'value 2', }; module.exports = MyConstructor(opts) { var options = extend({}, defaultOptions, opts); }
slide-38
SLIDE 38

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

slide-39
SLIDE 39

ABUSING VARIABLE ARGUMENTS

(3/22)

slide-40
SLIDE 40

PROBLEMS

Hard to make it work generally Error-prone

slide-41
SLIDE 41

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

  • ptions = { encoding: null, flag: 'r' };

} else if (typeof options === 'string') {

  • ptions = { encoding: options, flag: 'r' };

} else if (!options) {

  • ptions = { encoding: null, flag: 'r' };

} else if (typeof options !== 'object') { throw new TypeError('Bad arguments'); } var encoding = options.encoding; assertEncoding(encoding); // ...

slide-42
SLIDE 42

POOR USE OF MODULARITY

(4/22)

slide-43
SLIDE 43

Files with > 200 LoC Lots of scattered functions Low cohesion No reuse Testing is hard

slide-44
SLIDE 44

All the handlers for a given resource inside the same module Modules that have loosely related functions inside it because it's the only place these functions are being used.

slide-45
SLIDE 45

modules are cheap expose a documented interface try to keep modules under 200 LoC

slide-46
SLIDE 46

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)

slide-47
SLIDE 47

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({

  • pt: 'foobar',

actor: actorOpts });

slide-48
SLIDE 48

module.exports = Counter; function Counter() { this._counter = 0; } Counter.prototype.increment = function() { this._counter += 1; }; Counter.prototype.get = function() { return this._counter; };

slide-49
SLIDE 49

module.exports = function createCounter(options) { var counter = 0; function increment() { counter += 1; } function get() { return counter; } return { increment: increment, get: get, }; }

slide-50
SLIDE 50

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

slide-51
SLIDE 51

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

slide-52
SLIDE 52

IGNORING CALLBACK ERRORS

(6/22)

slide-53
SLIDE 53

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

slide-54
SLIDE 54

SOLUTIONS

slide-55
SLIDE 55

USE A LINTER

like ESLint and enable the rule http://eslint.org/docs/rules/handle-callback-err

slide-56
SLIDE 56

USE ASYNC OR SIMILAR

var 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! }

slide-57
SLIDE 57

USE PROMISES

doThis() .then(doThat). .catch(handleError); function handleError(err) { // .. handle error }

slide-58
SLIDE 58

THE KITCHEN-SINK MODULE

(7/22)

slide-59
SLIDE 59

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;

slide-60
SLIDE 60

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

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

slide-61
SLIDE 61
  • 1. Embrace modules
  • 2. Enforce SRP
  • 3. Externalise modules
  • 4. Individualised packaging
slide-62
SLIDE 62

initialization:

global.App = ...

from any file:

App.Models.Person.get(id);

slide-63
SLIDE 63

PLACING VALUES IN GLOBAL OBJECTS

(8/22)

slide-64
SLIDE 64

SYMPTOMS

Adding properties to any of these: process global GLOBAL root this on the global scope any other global reference, e.g. Buffer or console

slide-65
SLIDE 65

EXAMPLES

global.utilityFunction = function() { /*...*/ }; // or ... global.maxFoosticles = 10;

slide-66
SLIDE 66

PROBLEM

Dependencies become implicit instead of explicit. Makes the code harder to reason about for a newcomer

slide-67
SLIDE 67

SOLUTION

Leverage the module cache

slide-68
SLIDE 68

EXAMPLE

Create a file module:

exports.maxFoosticles = 10;

Require this file module in other files

var config = require('./config'); config.maxFoosticles // => 10

slide-69
SLIDE 69

EXAMPLE:

config.js:

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

slide-70
SLIDE 70

EXAMPLE 2

models/people.js

module.exports = new PeopleModel();

client:

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

slide-71
SLIDE 71

EXCEPTIONS

Testing framework ...?

slide-72
SLIDE 72

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

slide-73
SLIDE 73

SYNCHRONOUS EXECUTION AFTER INITIALISATION

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)

slide-74
SLIDE 74

SYMPTOMS

Higher request latency Performance decays quickly when under load

slide-75
SLIDE 75

fs.readFileSync fs.accessSync fs.changeModSync fs.chownSync fs.closeSync fs.existsSync ...

slide-76
SLIDE 76

Asynchronous initialisation

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

slide-77
SLIDE 77

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

slide-78
SLIDE 78

DANGLING SOURCE STREAM

(10/22)

slide-79
SLIDE 79

SYMPTOMS

When a stream throws an error or closes while piping, streams are not properly disposed and resources leak.

slide-80
SLIDE 80

SOLUTION

listen for error and close events on every stream and cleanup

  • r use the pump package instead of the native

stream.pipe()

slide-81
SLIDE 81

WRONG:

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

slide-82
SLIDE 82
slide-83
SLIDE 83
slide-84
SLIDE 84
slide-85
SLIDE 85
slide-86
SLIDE 86
slide-87
SLIDE 87

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);

slide-88
SLIDE 88

EVEN BETTER:

var pump = require('pump'); pump(mongoose.find().stream(), transform, res);

slide-89
SLIDE 89

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

slide-90
SLIDE 90

CHANGING THE WAY

require()

WORKS

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

(11/22)

slide-91
SLIDE 91

Setting NODE_PATH Using a module that requires in a different way. e.g. 'rootpath' 'require-root' 'app-root-path' 'root-require'

slide-92
SLIDE 92

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

slide-93
SLIDE 93
slide-94
SLIDE 94

THE MONOLITHIC APPLICATION

(12/22)

slide-95
SLIDE 95

NODE IS GREAT FOR PROTOTYPING

But this may become a trap

slide-96
SLIDE 96

EXAMPLES

Views and API on the same code base Services that do a lot of disjoint things

slide-97
SLIDE 97

SYMPTOMS

slide-98
SLIDE 98

POOR TEST COVERAGE

slide-99
SLIDE 99

BRITTLE IN SOME PARTS

slide-100
SLIDE 100

NOT MUCH ELBOW ROOM

slide-101
SLIDE 101

LONG DELIVERY CYCLES

and high error rate

slide-102
SLIDE 102

LONG TIME OF CODE ONBOARDING AND HAND-HOLDING

slide-103
SLIDE 103

HOW WE GET THERE

slide-104
SLIDE 104

EXAMPLE

slide-105
SLIDE 105
slide-106
SLIDE 106
slide-107
SLIDE 107
slide-108
SLIDE 108
slide-109
SLIDE 109
slide-110
SLIDE 110
slide-111
SLIDE 111
slide-112
SLIDE 112
slide-113
SLIDE 113

SOLUTIONS

slide-114
SLIDE 114

SEPARATE VIEWS FROM API

Embrace Cross-origin resource sharing.

slide-115
SLIDE 115

NODE IS GREAT AT NETWORKING

slide-116
SLIDE 116

SLOWLY MIGRATE

KEEP THE MONOLITH RUNNING

but develop new or updated features into separate smaller services

slide-117
SLIDE 117

TURN A MACRO-SERVICE INTO A SET OF MICRO-SERVICES

slide-118
SLIDE 118

CHALLENGES

Versioning, Testing, Shared Asset Management, Deploying, Service Lookup

slide-119
SLIDE 119

TESTING

slide-120
SLIDE 120

LITTLE OR NO AUTOMATED TESTS

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

(13/22)

slide-121
SLIDE 121

Project on-boarding takes a long time App is brittle, needs fixing in production all the time Developers are reluctant to make changes QA process doesn't seem strict enough QA cycle takes too long

slide-122
SLIDE 122

CAUSES

Lack of experience writing tests No testing culture Weak quality culture Management doesn't value tests "Wasting" time in automated tests is forbidden

slide-123
SLIDE 123

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

slide-124
SLIDE 124

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

slide-125
SLIDE 125

TESTING AT THE WRONG LEVEL

(14/22)

slide-126
SLIDE 126

SYMPTOMS

slide-127
SLIDE 127

UNIT TESTS THAT REACH INTO THE BOWELS OF A MODULE

slide-128
SLIDE 128

TESTING THE IMPLEMENTATION, NOT THE INTERFACE

slide-129
SLIDE 129

FILE MODULES EXPOSING EXTRA DETAILS

slide-130
SLIDE 130

CHANGING IMPLEMENTATION DETAILS

  • ften requires updating the tests
slide-131
SLIDE 131

CAUSES

slide-132
SLIDE 132

POOR USE OF MODULARITY

slide-133
SLIDE 133

POOR SEPARATION OF CONCERNS

slide-134
SLIDE 134

EXAMPLE

Testing a side effect:

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

slide-135
SLIDE 135

EXAMPLE

Invoking a private API:

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

slide-136
SLIDE 136

SOLUTIONS

slide-137
SLIDE 137

TEST AT THE INTERFACE LEVEL

All that the tests should require is

var mymodule = require('..')

slide-138
SLIDE 138

Test the behaviour not the implementation Don't conflate concerns on the same module Externalise: Make good use of NPM

slide-139
SLIDE 139

FACILITATE TESTING

By overriding default options

  • ptions.timeout = muchShorterValue;
slide-140
SLIDE 140

USE MOCKS, SPIES OR DEPENDENCY INJECTION FOR THIRD-PARTY PACKAGES

as a last resource and as long as you don't spy on internal stuff

slide-141
SLIDE 141

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); }; }

slide-142
SLIDE 142

FOCUS ON TESTING THE INTERFACE

slide-143
SLIDE 143

COLLABORATION IS HARD...

slide-144
SLIDE 144

DEPENDING ON GLOBALLY INSTALLED MODULES ON NPM SCRIPTS

Jane$ npm install -g lattemacchiato ... installed version 1.4 ... ... "scripts": { "test": "lattemacchiato --extra-sugar test/ }, ... Jane$ npm test lattemacchiato: all ok!

(15/22)

slide-145
SLIDE 145

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

slide-146
SLIDE 146

$ npm i --save-dev lattemacchiato ... "scripts": { "test": "lattemacchiato --extra-sugar test/" }, "devDependencies": { "lattemacchiato": "^1.4" ... $ ls node_modules/.bin lattemacchiato

slide-147
SLIDE 147
slide-148
SLIDE 148

USING GULP OR GRUNT INSTEAD OF NPM SCRIPTS

(16/22)

slide-149
SLIDE 149

SYMPTOMS

slide-150
SLIDE 150

LONG TIME SPENT ON TOOLING

slide-151
SLIDE 151

HARD TO CHANGE THE TASKS

slide-152
SLIDE 152

Start by using NPM scripts to automate tasks Use the package.json "pre" and "post" script hooks Use default config inside package.json Then run with

$ npm run mytask

slide-153
SLIDE 153

TYPICAL TASKS

Automated Tests Transformations Watching Live-reloading Starting service ...

slide-154
SLIDE 154

EXAMPLE

slide-155
SLIDE 155

{ "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"

slide-156
SLIDE 156

] }

slide-157
SLIDE 157

CHALLENGES

Make Windows-compatible scripts Know when to switch to gulp (instead of building a gulp-like system)

slide-158
SLIDE 158

NOT MEASURING CODE COVERAGE

(17/22)

slide-159
SLIDE 159

ISTANBUL

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

slide-160
SLIDE 160

"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" ]

slide-161
SLIDE 161

POOR USE OF NPM

(18/22)

slide-162
SLIDE 162

NPM is the biggest and fastest growing open source package repo Make good use of existing open-source

slide-163
SLIDE 163

Ignorance of existing modules NIH syndrome Reluctance with dependency management "My needs are unique"

slide-164
SLIDE 164

I need a testing framework that computes code coverage and sends coverage stats to coveralls.io ¯\_(ツ)_/¯

slide-165
SLIDE 165

I need a testing framework tap, mocha, lab I need to compute code coverage istanbul I need to send coverage stats to coveralls.io coveralls

slide-166
SLIDE 166

DISCOVERABILITY

libraries.io github mailing lists IRC social networks

slide-167
SLIDE 167

License Release frequency Last updated Open issues Test coverage Documentation quality

slide-168
SLIDE 168

SECURITY

Node Security Project — Snyk: https://nodesecurity.io/ https://snyk.io/

slide-169
SLIDE 169

PERFORMANCE IS HARD...

slide-170
SLIDE 170

PERFORMING CPU-HEAVY WORK

function myHandler(req, res) { var result = req.body.items.reduce(reducer); res.send(result); }

(19/22)

slide-171
SLIDE 171

Parsing (response from the database, response from external service) Computation-heavy work (like Natural Language Processing, Classification, Learning, etc.) Processing big sequences of data Mapping or a big dataset Aggregating a big dataset Calculating an HMAC for a big document

slide-172
SLIDE 172
slide-173
SLIDE 173

UNLIMITED ASYNCHRONOUS ITERATIONS

#performance #reliability (20/22)

slide-174
SLIDE 174

SYMPTOMS

High request latency at times

slide-175
SLIDE 175

REASONS

The event loop is busy leads to application hickups.

slide-176
SLIDE 176

EXAMPLE

async.each instead of async.eachLimit async.map instead of async.mapLimit

slide-177
SLIDE 177

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(...) } }); }

slide-178
SLIDE 178

SACRIFICE RESPONSE TIME FOR THE GREATER GOOD AND 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(...) } }); }

slide-179
SLIDE 179

LARGE DENORMALISED DOCUMENTS

{ _id: ... items: [ { itemId: ..., } ], history: [ ... ] }

(21/22)

slide-180
SLIDE 180

Large memory consumption Request latency spikes

slide-181
SLIDE 181

Improve the schema Stream Minimise marshalling

slide-182
SLIDE 182

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

slide-183
SLIDE 183

MISSING THE OPPORTUNITY OF USING STREAMS

#reliability #performance #maintainability (22/22)

slide-184
SLIDE 184

SYMPTOMS

Response time bubbles High memory consumption

slide-185
SLIDE 185

EXAMPLE

Buffering 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); } };

slide-186
SLIDE 186

Now, streaming:

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

slide-187
SLIDE 187

CHALLENGES

Streams API Error handling (header is sent before the body)

slide-188
SLIDE 188

BENEFITS

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

slide-189
SLIDE 189

MAIN TAKE-AWAY'S

Node is fundamentally different from the other technologies frequently used in big teams. Adopting Node also means adopting its newer practices. More at blog.yld.io

slide-190
SLIDE 190

PEDRO TEIXEIRA

@pgte — pedro@yld.io

IGOR SOAREZ

@igorsoarez — igor@yld.io

slide-191
SLIDE 191
slide-192
SLIDE 192

THANK YOU!

slide-193
SLIDE 193

Q&A