Dependency Injection in Node.js

Erin Noe-Payne

Who here has...

  • has built a node app with more than 1 file?
  • written that one giant file...?
  • writes unit tests against their code?

Why do we care about DI?

  • Promotes good design
  • Single Responsibility, Open / Closed principles
  • Unit testing

What's out there?

Hijack `require`

"use strict";

require('../example-utils').listModuleAndTests(__dirname + '/foo.js', __filename);

var proxyquire =  require('../..')
  , assert     =  require('assert')
  , pathStub   =  { };

// when not overridden, path.extname behaves normally
var foo = proxyquire('./foo', { 'path': pathStub });
assert.equal(foo.extnameAllCaps('file.txt'), '.TXT');

// override path.extname
pathStub.extname = function (file) { return 'Exterminate, exterminate the ' + file; };

// path.extname now behaves as we told it to
assert.equal(foo.extnameAllCaps('file.txt'), 'EXTERMINATE, EXTERMINATE THE FILE.TXT');

// path.basename and all other path module methods still function as before
assert.equal(foo.basenameAllCaps('/a/b/file.txt'), 'FILE.TXT');

console.log('*** All asserts passed ***');

IoC Container

var di = require('di');
var Engine = require('./engine');

var Car = function(engine) {
  this.engine = engine;
};

Car.prototype = {
  run: function() {
    this.engine.start();
  },
  isRunning: function() {
    return this.engine.state === 'running';
  }
};

di.annotate(Car, new di.Inject(Engine));

module.exports = Car;

So what are we using?

  • nject
  • express-train

Nject

var nject = require('nject');
var tree = new nject.Tree();

tree.constant('a', 7);
tree.constant('b', 9);

tree.register('sum',
    /*
    variable names matter!
    a will be injected with nject's registered constant a
    b will be injected with constant b
    */
    function(a, b) {
        return a+b;
    });

tree.register('asyncDifference',
    /*
    this module resolves asynchronously, 
    because it is injected with the reserved word _done
    */
    function(a, b, _done) {
        setTimeout(function(){
          _done(b-a)
        }, 1000)
    });

tree.register('printer',
    function(sum, asyncDifference) {
        console.log(asyncDifference);
        console.log(sum);
    });

tree.resolve(function(err, resolved){
  //all are true...
  resolved.a == 7
  resolved.b == 9
  resolved.sum == 16
  resolved.asyncDifference == 2
  resolved.printer == undefined
});

// console logs from printer:
// 2
// 16

Express-train

//index.js
train = require('express-train')
tree = train({
    base : __dirname,
    files : [
        '**/*.js',
        '!{public, views, test}/**'
    ]
})

tree.resolve()
//models/Users.js
var mongoose = require('mongoose');

/* This module has no dependencies */
module.exports = function () {
    var UserSchema = new mongoose.Schema({
        username:{ type:String, required:true, unique:true },
        email:{ type:String, required:false, unique:false },
        password:{ type:String, required:true}
    });

    /* This return value is what will be injected when Users is referenced */
    return mongoose.model('users', UserSchema);
}

Yeah, yeah, but what does it REALLY look like?

Lessons Learned

  • Unique file names...
  • To DI, or not to DI?
  • Async resolution - worth it?

Questions?

Made with Slides.com