Bonnes pratiques de
développement avec Node.js
              François Zaninotto
     @francoisz http://github.com/fzaninotto
Node.js ne suffit pas
et vous ne pouvez pas réinventer la roue
Organisation du code
        Objectif: éviter le code spaghetti
Utiliser un framework pour les applis web
    visionmedia/express (ou viatropos/tower)

Eviter la course aux callbacks
           caolan/async (ou kriskowal/q)

Inclure des routes et monter des sous-applications
«Fat model, Skinny controller»
Modules de service pour éviter un modèle trop fat
// main app.js
var express = require('express');
var app = module.exports = express.createServer();

app.configure(function(){
  app.use(app.router);
  // these middlewares are required by some of the mounted apps
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.cookieParser());
  app.use(express.session({ secret: 'qdfegfskqdjfhskjdfh' }));
});

// Routes
app.use('/api',       require('./app/api/app'));
app.use('/dashboard', require('./app/dashboard/app'));
app.get('/', function(reaq, res) {
  res.redirect('/dashboard/events');
});

app.listen(3000);
// dashboard app
var express = require('express');
var app = module.exports = express.createServer();

app.configure(function(){
  app.use(app.router);
  app.set('views', __dirname + '/views');
  app.set('view engine', 'ejs');
  app.use(express.static(__dirname + '/public'));
});

// Routes
app.get('/events', function(req, res) {
  res.render('events', { route: app.route });
});
app.get('/checks', function(req, res) {
  res.render('checks', { route: app.route, info: req.flash('info')});
});
//...
if (!module.parent) {
  app.listen(3000);
}
// dashboard app
var express = require('express');
var app = module.exports = express.createServer();

app.configure(function(){
  app.use(app.router);
  app.set('views', __dirname + '/views');
  app.set('view engine', 'ejs');
  app.use(express.static(__dirname + '/public'));
});

// Routes
app.get('/events', function(req, res) {
  res.render('events', { route: app.route });
});
app.get('/checks', function(req, res) {
  res.render('checks', { route: app.route, info: req.flash('info')});
});
//...
if (!module.parent) {
  app.listen(3000);
}
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Uptime</title>
    <link rel="stylesheet" href="<%= route %>/stylesheets/bootstrap.css">
    <link rel="stylesheet" href="<%= route %>/stylesheets/style.css">
  </head>
  <body>
    <div class="navbar">
      <div class="navbar-inner">
        <div class="container">
          <a class="brand" href="<%= route %>/events">Uptime</a>
          <ul class="nav pull-left">
            <li><a href="<%= route %>/events">Events</a></li>
            <li><a href="<%= route %>/checks">Checks</a></li>
            <li><a href="<%= route %>/tags">Tags</a></li>
          </ul>
        </div>
      </div>
    </div>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Uptime</title>
    <link rel="stylesheet" href="<%= route %>/stylesheets/bootstrap.css">
    <link rel="stylesheet" href="<%= route %>/stylesheets/style.css">
  </head>
  <body>
    <div class="navbar">
      <div class="navbar-inner">
        <div class="container">
          <a class="brand" href="<%= route %>/events">Uptime</a>
          <ul class="nav pull-left">
            <li><a href="<%= route %>/events">Events</a></li>
            <li><a href="<%= route %>/checks">Checks</a></li>
            <li><a href="<%= route %>/tags">Tags</a></li>
          </ul>
        </div>
      </div>
    </div>
Standards
     Felix's Node.js Style Guide
 (http://nodeguide.com/style.html)


Object-Oriented Programming FTW


     Domain-Driven Design!
var mongoose = require('mongoose'),
    Schema   = mongoose.Schema,
    async    = require('async');

var Check = new Schema({
    name        : String
  , type        : String
  , url         : String
  , interval    : { type: Number, default: 60000 }
  , maxTime     : { type: Number, default: 1500 }
  , tags        : [String]
  , lastChanged : Date
  , lastTested : Date
  , isUp        : Boolean
  , uptime      : { type: Number, default: 0 }
  , downtime    : { type: Number, default: 0 }
});
Check.plugin(require('../lib/lifecycleEventsPlugin'));
var mongoose = require('mongoose'),
    Schema   = mongoose.Schema,
    async    = require('async');

var Check = new Schema({
    name        : String
  , type        : String
  , url         : String
  , interval    : { type: Number, default: 60000 }
  , maxTime     : { type: Number, default: 1500 }
  , tags        : [String]
  , lastChanged : Date
  , lastTested : Date
  , isUp        : Boolean
  , uptime      : { type: Number, default: 0 }
  , downtime    : { type: Number, default: 0 }
});
Check.plugin(require('../lib/lifecycleEventsPlugin'));
var mongoose = require('mongoose');
var Schema   = mongoose.Schema;
var async    = require('async');

var Check = new Schema({
  name        : String,
  type        : String,
  url         : String,
  interval    : { type: Number, default: 60000 },
  maxTime     : { type: Number, default: 1500 },
  tags        : [String],
  lastChanged : Date,
  lastTested : Date,
  isUp        : Boolean,
  uptime      : { type: Number, default: 0 },
  downtime    : { type: Number, default: 0 },
});
Check.plugin(require('../lib/lifecycleEventsPlugin'));
Canaux de communication
                Dans un module
                                  Entre applications Client / Serveur
               ou une application

                   Appels de
Notifications                           Events            socket.io
                   méthodes

Echanges de        Appels de                            XMLHTTP
                                     API HTTP
  données          méthode                               Request

Traitements
                    Promise            AMQP              Mentir*
asynchrones
// server-side
var socketIo   = require('socket.io');
var CheckEvent = require('./models/checkEvent');

var io = socketIo.listen(app);

CheckEvent.on('postInsert', function(event) {
  io.sockets.emit('CheckEvent', event.toJSON());
});
// client-side
<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="/socket.io/socket.io.js"></script>
    <script>var socket = io.connect('http://'+location.hostname);</script>
  </head>
  <body>
    <div class="navbar"><ul id="counts"></ul></div>
    <script>
    $(document).ready(function() {
      var updateCounts = function() {
         $.getJSON('/api/check/count', function(count) {
           // display counts in navbar
         });
      };
      updateCounts();
      socket.on('CheckEvent', function() {
         updateCounts();
         $('#count').fadeOut().fadeIn().fadeOut().fadeIn();
      });
    });
    </script>
  </body>
</html>
Gestions des erreurs
try {} catch {} ne marche pas en asynchrone
function (err, results) est la signature
standard des callbacks asynchrones
"Leave early"
Utiliser les codes d’erreur HTTP pour les APIs
app.delete('/check/:id', function(req, res, next) {
  Check.findOne({ _id: req.params.id }, function(err, check) {
    if (err) {
      return next(err);
    }
    if (!check) {
      return next(new Error('No check with id ' + req.params.id));
    }
    check.remove(function(err2){
      if (err2) {
        req.flash('error', 'Error - Check not deleted');
        res.redirect('/checks');
        return;
      }
      req.flash('info', 'Check has been deleted');
      res.redirect('/checks');
    });
  });
});
Tests Unitaires
Librairies (presque) standard
    Assertions : visionmedia/should.js   (ou node/assert)

    TDD : visionmedia/mocha (ou caolan/node-unit)
    BDD : visionmedia/mocha (ou mhevery/jasmine-node)

L’asynchrone se teste aussi très bien
Pas besoin de mocker quand on peut monkey-patcher
describe('Connection', function(){
  var db = new Connection
    , tobi = new User('tobi')
    , loki = new User('loki')
    , jane = new User('jane');

     beforeEach(function(done){
        db.clear(function(err){
          if (err) return done(err);
          db.save([tobi, loki, jane], done);
        });
     })

     describe('#find()', function(){
        it('respond with matching records', function(done){
           db.find({ type: 'User' }, function(err, res){
             if (err) return done(err);
              res.should.have.length(3);
              done();
           })
        })
     })
})
var nock = require('nock');

var couchdb = nock('http://myapp.iriscouch.com')
  .get('/users/1')
  .reply(200, {
     _id: "123ABC",
     _rev: "946B7D1C",
     username: 'pgte',
     email: 'pedro.teixeira@gmail.com'}
  );
Projet open-source
Configurable
  app.configure()   pas compatible avec un SCM
  lorenwest/node-config (ou flatiron/nconf)

Extensible
  Custom events
  Plugin architecture
// in main app.js
path.exists('./plugins/index.js', function(exists) {
  if (exists) {
     require('./plugins').init(app, io, config);
  };
});

// in plugins/index.js
exports.init = function(app, io, config) {
  require('./console').init();
}
module.exports = exports = function lifecycleEventsPlugin(schema) {
   schema.pre('save', function (next) {
     var model = this.model(this.constructor.modelName);
     model.emit('preSave', this);
     this.isNew ? model.emit('preInsert', this) :
                  model.emit('preUpdate', this);
     this._isNew_internal = this.isNew;
     next();
   });
   schema.post('save', function() {
     var model = this.model(this.constructor.modelName);
     model.emit('postSave', this);
     this._isNew_internal ? model.emit('postInsert', this) :
                            model.emit('postUpdate', this);
     this._isNew_internal = undefined;
   });
   schema.pre('remove', function (next) {
     this.model(this.constructor.modelName).emit('preRemove', this);
     next();
   });
   schema.post('remove', function() {
     this.model(this.constructor.modelName).emit('postRemove', this);
   });
};
Questions ?


         François Zaninotto
@francoisz http://github.com/fzaninotto

Bonnes pratiques de développement avec Node js

  • 1.
    Bonnes pratiques de développementavec Node.js François Zaninotto @francoisz http://github.com/fzaninotto
  • 4.
    Node.js ne suffitpas et vous ne pouvez pas réinventer la roue
  • 5.
    Organisation du code Objectif: éviter le code spaghetti Utiliser un framework pour les applis web visionmedia/express (ou viatropos/tower) Eviter la course aux callbacks caolan/async (ou kriskowal/q) Inclure des routes et monter des sous-applications «Fat model, Skinny controller» Modules de service pour éviter un modèle trop fat
  • 6.
    // main app.js varexpress = require('express'); var app = module.exports = express.createServer(); app.configure(function(){ app.use(app.router); // these middlewares are required by some of the mounted apps app.use(express.bodyParser()); app.use(express.methodOverride()); app.use(express.cookieParser()); app.use(express.session({ secret: 'qdfegfskqdjfhskjdfh' })); }); // Routes app.use('/api', require('./app/api/app')); app.use('/dashboard', require('./app/dashboard/app')); app.get('/', function(reaq, res) { res.redirect('/dashboard/events'); }); app.listen(3000);
  • 7.
    // dashboard app varexpress = require('express'); var app = module.exports = express.createServer(); app.configure(function(){ app.use(app.router); app.set('views', __dirname + '/views'); app.set('view engine', 'ejs'); app.use(express.static(__dirname + '/public')); }); // Routes app.get('/events', function(req, res) { res.render('events', { route: app.route }); }); app.get('/checks', function(req, res) { res.render('checks', { route: app.route, info: req.flash('info')}); }); //... if (!module.parent) { app.listen(3000); }
  • 8.
    // dashboard app varexpress = require('express'); var app = module.exports = express.createServer(); app.configure(function(){ app.use(app.router); app.set('views', __dirname + '/views'); app.set('view engine', 'ejs'); app.use(express.static(__dirname + '/public')); }); // Routes app.get('/events', function(req, res) { res.render('events', { route: app.route }); }); app.get('/checks', function(req, res) { res.render('checks', { route: app.route, info: req.flash('info')}); }); //... if (!module.parent) { app.listen(3000); }
  • 9.
    <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Uptime</title> <link rel="stylesheet" href="<%= route %>/stylesheets/bootstrap.css"> <link rel="stylesheet" href="<%= route %>/stylesheets/style.css"> </head> <body> <div class="navbar"> <div class="navbar-inner"> <div class="container"> <a class="brand" href="<%= route %>/events">Uptime</a> <ul class="nav pull-left"> <li><a href="<%= route %>/events">Events</a></li> <li><a href="<%= route %>/checks">Checks</a></li> <li><a href="<%= route %>/tags">Tags</a></li> </ul> </div> </div> </div>
  • 10.
    <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Uptime</title> <link rel="stylesheet" href="<%= route %>/stylesheets/bootstrap.css"> <link rel="stylesheet" href="<%= route %>/stylesheets/style.css"> </head> <body> <div class="navbar"> <div class="navbar-inner"> <div class="container"> <a class="brand" href="<%= route %>/events">Uptime</a> <ul class="nav pull-left"> <li><a href="<%= route %>/events">Events</a></li> <li><a href="<%= route %>/checks">Checks</a></li> <li><a href="<%= route %>/tags">Tags</a></li> </ul> </div> </div> </div>
  • 11.
    Standards Felix's Node.js Style Guide (http://nodeguide.com/style.html) Object-Oriented Programming FTW Domain-Driven Design!
  • 12.
    var mongoose =require('mongoose'), Schema = mongoose.Schema, async = require('async'); var Check = new Schema({ name : String , type : String , url : String , interval : { type: Number, default: 60000 } , maxTime : { type: Number, default: 1500 } , tags : [String] , lastChanged : Date , lastTested : Date , isUp : Boolean , uptime : { type: Number, default: 0 } , downtime : { type: Number, default: 0 } }); Check.plugin(require('../lib/lifecycleEventsPlugin'));
  • 13.
    var mongoose =require('mongoose'), Schema = mongoose.Schema, async = require('async'); var Check = new Schema({ name : String , type : String , url : String , interval : { type: Number, default: 60000 } , maxTime : { type: Number, default: 1500 } , tags : [String] , lastChanged : Date , lastTested : Date , isUp : Boolean , uptime : { type: Number, default: 0 } , downtime : { type: Number, default: 0 } }); Check.plugin(require('../lib/lifecycleEventsPlugin'));
  • 14.
    var mongoose =require('mongoose'); var Schema = mongoose.Schema; var async = require('async'); var Check = new Schema({ name : String, type : String, url : String, interval : { type: Number, default: 60000 }, maxTime : { type: Number, default: 1500 }, tags : [String], lastChanged : Date, lastTested : Date, isUp : Boolean, uptime : { type: Number, default: 0 }, downtime : { type: Number, default: 0 }, }); Check.plugin(require('../lib/lifecycleEventsPlugin'));
  • 15.
    Canaux de communication Dans un module Entre applications Client / Serveur ou une application Appels de Notifications Events socket.io méthodes Echanges de Appels de XMLHTTP API HTTP données méthode Request Traitements Promise AMQP Mentir* asynchrones
  • 16.
    // server-side var socketIo = require('socket.io'); var CheckEvent = require('./models/checkEvent'); var io = socketIo.listen(app); CheckEvent.on('postInsert', function(event) { io.sockets.emit('CheckEvent', event.toJSON()); });
  • 17.
    // client-side <!DOCTYPE html> <htmllang="en"> <head> <script src="/socket.io/socket.io.js"></script> <script>var socket = io.connect('http://'+location.hostname);</script> </head> <body> <div class="navbar"><ul id="counts"></ul></div> <script> $(document).ready(function() { var updateCounts = function() { $.getJSON('/api/check/count', function(count) { // display counts in navbar }); }; updateCounts(); socket.on('CheckEvent', function() { updateCounts(); $('#count').fadeOut().fadeIn().fadeOut().fadeIn(); }); }); </script> </body> </html>
  • 18.
    Gestions des erreurs try{} catch {} ne marche pas en asynchrone function (err, results) est la signature standard des callbacks asynchrones "Leave early" Utiliser les codes d’erreur HTTP pour les APIs
  • 19.
    app.delete('/check/:id', function(req, res,next) { Check.findOne({ _id: req.params.id }, function(err, check) { if (err) { return next(err); } if (!check) { return next(new Error('No check with id ' + req.params.id)); } check.remove(function(err2){ if (err2) { req.flash('error', 'Error - Check not deleted'); res.redirect('/checks'); return; } req.flash('info', 'Check has been deleted'); res.redirect('/checks'); }); }); });
  • 20.
    Tests Unitaires Librairies (presque)standard Assertions : visionmedia/should.js (ou node/assert) TDD : visionmedia/mocha (ou caolan/node-unit) BDD : visionmedia/mocha (ou mhevery/jasmine-node) L’asynchrone se teste aussi très bien Pas besoin de mocker quand on peut monkey-patcher
  • 21.
    describe('Connection', function(){ var db = new Connection , tobi = new User('tobi') , loki = new User('loki') , jane = new User('jane'); beforeEach(function(done){ db.clear(function(err){ if (err) return done(err); db.save([tobi, loki, jane], done); }); }) describe('#find()', function(){ it('respond with matching records', function(done){ db.find({ type: 'User' }, function(err, res){ if (err) return done(err); res.should.have.length(3); done(); }) }) }) })
  • 22.
    var nock =require('nock'); var couchdb = nock('http://myapp.iriscouch.com') .get('/users/1') .reply(200, { _id: "123ABC", _rev: "946B7D1C", username: 'pgte', email: 'pedro.teixeira@gmail.com'} );
  • 23.
    Projet open-source Configurable app.configure() pas compatible avec un SCM lorenwest/node-config (ou flatiron/nconf) Extensible Custom events Plugin architecture
  • 24.
    // in mainapp.js path.exists('./plugins/index.js', function(exists) { if (exists) { require('./plugins').init(app, io, config); }; }); // in plugins/index.js exports.init = function(app, io, config) { require('./console').init(); }
  • 25.
    module.exports = exports= function lifecycleEventsPlugin(schema) { schema.pre('save', function (next) { var model = this.model(this.constructor.modelName); model.emit('preSave', this); this.isNew ? model.emit('preInsert', this) : model.emit('preUpdate', this); this._isNew_internal = this.isNew; next(); }); schema.post('save', function() { var model = this.model(this.constructor.modelName); model.emit('postSave', this); this._isNew_internal ? model.emit('postInsert', this) : model.emit('postUpdate', this); this._isNew_internal = undefined; }); schema.pre('remove', function (next) { this.model(this.constructor.modelName).emit('preRemove', this); next(); }); schema.post('remove', function() { this.model(this.constructor.modelName).emit('postRemove', this); }); };
  • 26.
    Questions ? François Zaninotto @francoisz http://github.com/fzaninotto