Node Fundamentals

Agenda

  • Introduction to Node
  • Express
  • MongoDB

Introduction to Node

What is Node?

  • Node is JavaScript running on the server-side
  • Powered by Google V8 runtime
  • Fast, scalable and extendable
  • Node is a platform not a framework
  • Created in 2009 by Ryan Dahl

Node Principles

  • Event-driven non-blocking I/O model
  • Single threaded
  • Asynchronous (callbacks)
  • Scale horizontally (more servers) instead vertically (more power)
  • Node apps are written in JavaScript: The same language in client and in server 

When use Node

  • Ideal for data intensive real time applications (many requests)
  • Bad for heavy calculations apps

Some Node examples

//Read file contents
var fs = require('fs');
fs.readFile('/etc/passwd', function (er, data) {
  console.log(data);
});

//Simple hello world server
var http = require('http');
var server = http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
});
server.listen(3000);
console.log('Server running at http://localhost:3000/');

//Alternative way: using streams
var http = require('http');
var server = http.createServer();
server.on('request', function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
})
server.listen(3000);
console.log('Server running at http://localhost:3000/');

Some Node examples (ii)

//piping the results
var http = require('http');
var fs = require('fs');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'image/png'});
  fs.createReadStream('./image.png').pipe(res);
}).listen(3000);
console.log('Server running at http://localhost:3000/');


//Event Emitters
var EventEmitter = require('events').EventEmitter;
var logger = new EventEmitter();
logger.on('error', function(message){
  console.log('ERR: ' + message);
});
logger.emit('error', 'Spilled Milk');
logger.emit('error', 'Eggs Cracked');

//Copy a file
var fs = require('fs');
var file = fs.createReadStream("readme.md");
var newFile = fs.createWriteStream("readme_copy.md");
file.pipe(newFile);

Some Node Examples (iii)

//node passing style <- bad
function someAsyncProcess(data, callback) {
  doAsyncProcess1(data, function(err, result1) {
    if (err) {
      callback(err);
    } else {
      doAsyncProcess2(result1, function(err, result2) {
        if (err) {
          callback(err);
        } else {
          callback(null, result2);
        }
      });
    } 
  });
}
//node passing style <- better
function someAsyncProcess(data, callback) {
  doAsyncProcess1(data, function(err, result1) {
    if (err) return callback(err);
    
    doAsyncProcess2(result1, callback);
  });
}

Streams examples

var Readable = require('stream').Readable;

var rs = new Readable;
rs.push('beep ');
rs.push('boop\n');
rs.push(null);

rs.pipe(process.stdout);

Extracted from here

//The producer sends data as the consumer is requesting
//to test node test.js | head -c5
var Readable = require('stream').Readable;
var rs = Readable();

var c = 97 - 1;

rs._read = function () {
    if (c >= 'z'.charCodeAt(0)) return rs.push(null);

    setTimeout(function () {
        rs.push(String.fromCharCode(++c));
    }, 100);
};

rs.pipe(process.stdout);

process.on('exit', function () {
    console.error('\n_read() called ' + (c - 97) + ' times');
});
process.stdout.on('error', process.exit);

Streams Examples (ii)


var fs = require('fs');
var ws = fs.createWriteStream('message.txt');

ws.write('beep ');

setTimeout(function () {
    ws.end('boop\n');
}, 1000);
  • process
  •  child_process.spawn()
  •  fs
  • net
  • http
  • zlib
  • ...

 

More on Node Streams Playground

Built-in Streams

Node modules

var http = require('http');
var fs = require('fs');

Core modules

//hello.js
var hello = function() {
  console.log("hello!");
};

module.exports = hello;

Custom modules

//foo.js
var foo = function() { ... };
var bar = function() { ... };
var baz = function() { ... };

module.exports = {
  foo: foo,
  bar: bar,
  baz: baz
};
//app.js
require('./hello')();

var myMod = require('./foo');
myMod.foo();
myMod.bar();
npm install --save express

Third-party modules

//package.json
{
  ...
  "dependencies": {
    "express": "*",
    "optimist": ">= 0.1.0"
    ...
  }
}
//app.js
require('express');
$ find .
...
./package.json
./node_modules
./node_modules/express
...

Express

What is Express?

  • Sinatra inspired web development framework for Node.js
  • Fast, flexible, and simple
  • Provides a set of features for building web app's
    • Routing
    • Middleware
    • Environment based configuration
    • Views and templates
    • Security

Routing without Express

var http = require('http');
http.createServer(function(req,res){
  var path = req.url.replace(/\/?(?:\?.*)?$/, '').toLowerCase();
  switch(path) {
    case '':
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.end('Homepage');
      break;
  case '/about':
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('About');
    break;
  default:
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
}
}).listen(3000);

Routing with Express

var express = require('express');

var app = express();

app.set('ip', process.env.IP || '0.0.0.0');
app.set('port', process.env.PORT || 3000);

// homepage page
app.get('/', function(req, res){
  res.type('text/plain');
  res.status(200);
  res.send('Homepage');
});

// about page
app.all('/about', function(req, res){
  res.type('text/plain');
  res.status(200);
  res.send('About');
});

// custom 404 page
app.use(function(req, res){
  res.type('text/plain');
  res.status(404);
  res.send('404 - Not Found');
});

app.listen(app.get('port'), app.get('ip'), function(){
  console.log( 'Express started on http://' + app.get('ip') + ':' + app.get('port'));
});

Static files

app.use(express.static(__dirname + '/public'));

CRUD REST operations

var express = require('express');
var bodyParser = require('body-parser');

var app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

app.set('ip', process.env.IP || '0.0.0.0');
app.set('port', process.env.PORT || 3000);

var scores = {};
var numScores = 0;

/* ROUTES */
app.post('/score', createScore);
app.get('/score', getAll);
app.get('/score/:scoreId', getScore);
app.delete('/score/:scoreId', delScore);
app.put('/score/:scoreId', updateScore);
/* END ROUTES */

function createScore(req, res) {
  var _id = String(numScores);
  var score = { _id: _id, home: 0, guest: 0};	
  scores[_id] = score;
  numScores++;  
  res.json(score);
}

function getAll(req, res, next) {
  res.json(scores);
}
function getScore(req, res, next) {
  var scoreId = req.params.scoreId;
  if (scores[scoreId]) {
    res.json(scores[scoreId]);
  } else {
    next(new Error('score ' + scoreId + ' not exists'));
  }
}

function delScore(req, res, next) {
  delete scores[req.params.scoreId];
  res.send('scored ' + req.params.scoreId + ' removed.');
}

function updateScore(req, res, next) {
  var scoreId = req.params.scoreId;
  if (scores[scoreId]) {
    var newScore = {
      _id: req.params.scoreId,
      home: req.body.home,
      guest: req.body.guest
    };
    scores[req.params.scoreId] = newScore;
    res.json(newScore);
  } else {
    next(new Error('score ' + scoreId + ' not exists'));
  }
}

app.listen(app.get('port'), app.get('ip'), function(){
  console.log( 'Express started ...');
});

Routers

var express = require('express');
var bodyParser = require('body-parser');
var fs = require('fs');

var app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

//ROUTERS
app.use('/score', require('./score'));

app.set('ip', process.env.IP || '0.0.0.0');
app.set('port', process.env.PORT || 3000);

app.listen(app.get('port'), app.get('ip'), function(){
  console.log( 'Express started ...');
});
var express = require('express');
var router = express.Router();

var scores = {};
var numScores = 0;

/* ROUTES */
router.post('/', createScore);
router.get('/', getAll);
router.get('/:scoreId', getScore);
router.delete('/:scoreId', delScore);
router.put('/:scoreId', updateScore);
/* END ROUTES */

/* PARAMS */
router.param('scoreId', checkScoreExists);
/* END PARAMS */

function checkScoreExists (req, res, next, scoreId) {
  if (scores[scoreId]) {
    req.score = scores[scoreId];
    next();
  } else {
    next(new Error(scoreId + ' not exists'));
  }
}

function getScore(req, res, next) {
  res.json(req.score);
}

...

module.exports = router;

request parameters

...
app.post('/test/:param1/:param2', test);

function test(req, res) {
  var params = {
    path: req.params,
    query: req.query,
    body: req.body,
    token: req.header('Authorization')
  };
  
  res.json(params);
}
...
$ curl -X POST -d 'paramFoo=bar' -H 'Authorization: Bearer mytoken123' \
'http://localhost:5000/test/value1/value2?paramX=value3¶mY=value4'

{
  "path": {
    "param1": "value1",
    "param2": "value2"
  },
  "query": {
    "paramX": "value3¶mY=value4"
  },
  "body": {
    "paramFoo": "bar"
  },
  "token": "Bearer mytoken123"
}

Middleware

  • Conceptually, middleware is a way to encapsulate functionality
  • Practically, it is simply a function that takes three arguments:
    • a request object,
    • a response object,
    • and a next() function
  • Middlewares are executed in definition order: like a pipeline
  • In an Express app, you insert middleware into the pipeline by
    calling app.use(...)
  • If you don’t call next() , the pipeline will be terminated, and no more route handlers or middleware will be processed
  • If you don’t call next() , you should send a response to the client ( res.send , res.json , res.render , etc.)
  • if you don’t, the client will hang and eventually time out

Middleware (ii)

var app = express();
...
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

// a middleware with no mount path; gets executed for every request to the app
app.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});

// a middleware mounted on /test; will be executed for any type of HTTP request to /test
app.use('/test', function (req, res, next) {
  console.log('Request Type:', req.method);
  next();
});

app.all('/test*', test);

function test(req, res) {
  res.send('ok');
}
...

Common middleware

  • basicAuth: Provides basic access authorization
  • body-parser: Convenience middleware that simply links in json and urlencoded 
  • json: Parses JSON-encoded request bodies
  • urlencoded: Parses request bodies with media type application/x-www-form-urlencoded
  • compress: Compresses response data with gzip
  • cookie-parser: Provides cookie support
  • express-session: Provides session ID (stored in a cookie) session support
  • csurf: Provides protection against cross-site request forgery (CSRF) attacks
  • directory: Provides directory listing support for static files
  • errorhandler: Provides stack traces and error messages to the client

Common middleware (ii)

  • static-favicon: Servers favicon icon
  • morgan: Provides automated logging support
  • method-override: allows browsers to “fake” using HTTP methods other than GET and POST
  • static: Provides support for serving static (public) files
  • vhost: makes subdomains easier to manage in Express

Third-Party Middleware

There are other middleware that can be added to Express. You can find them by doing a search in npm 

Error handling

//Define error-handling middleware like other middleware, except with four arguments
//instead of three, specifically with the signature (err, req, res, next)):
app.use(function(err, req, res, next){
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

//Though not strictly required, by convention you define error-handling middleware last,
//after other app.use() calls; For example:
var bodyParser = require('body-parser');
var methodOverride = require('method-override');

app.use(bodyParser());
app.use(methodOverride());
app.use(function(err, req, res, next){
  // logic
});

Error handling (ii)

//For organizational (and higher-level framework) purposes,
//you may define several error-handling middleware
var bodyParser = require('body-parser');
var methodOverride = require('method-override');

app.use(bodyParser());
app.use(methodOverride());
app.use(logErrors);
app.use(clientErrorHandler);
app.use(errorHandler);

//Where the more generic logErrors may write request and
//error information to stderr, loggly, or similar services:
function logErrors(err, req, res, next) {
  console.error(err.stack);
  next(err);
}

Multiple handlers

var express = require('express');
var bodyParser = require('body-parser');
var fs = require('fs');

var app = express();
app.get('/foo',
  function(req, res, next){
    if(Math.random() < 0.33) return next();
    res.send('red');
  },
  function(req, res, next){
    if(Math.random() < 0.5) return next();
    res.send('green');
  },
  function(req, res){
    res.send('blue');
  }
);
app.set('ip', process.env.IP || '0.0.0.0');
app.set('port', process.env.PORT || 3000);

app.listen(app.get('port'), app.get('ip'), function(){
  console.log( 'Express started ...');
});

Routing pattern matching

//maps /user and /username
app.get('/user(name)?', function(req,res){
  res.render('user');
});


//maps /khaan /khaaaaaaan, etc
app.get('/khaa+n', function(req,res){
  res.render('khaaan');
});


//maps /crazy /mad /madness /lunacy
app.get(/crazy|mad(ness)?|lunacy/, function(req,res){
  res.render('madness');
});

Authentication

var express = require('express');
var router = express.Router();
var passport = require('passport');
var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
var jwtSecret = require('../util/config').jwtSecret;
var request = require('request');
var jwt = require('jsonwebtoken');

/* ROUTES */
router.get('/login', passportLogin());
router.get('/oauth2callback', passportCallback(), oauth2Callback);
router.post('/rt', refreshToken);
/* END ROUTES */

function passportLogin() {
 return passport.authenticate('google', {
  session: false,
  scope: config.scopes,
  accessType: 'offline'});
}

function passportCallback() {
 return passport.authenticate('google', {session: false, failureRedirect: '/auth/login'});
}

function oauth2Callback(req, res) {
 var token = jwt.sign(req.user, jwtSecret);
 var url = '/redirecting.html#token=' + token;
 res.redirect(url);
}

function refreshToken(req, res) {
 var rt = req.query.rt;
 if (!rt) {
  throw new Error('No valid token found');
 } else {
  request(
      {
       url: 'https://accounts.google.com/o/oauth2/token',
       form: {
        client_id: config.client_id,
        client_secret: config.client_secret,
        grant_type: 'refresh_token',
        refresh_token: rt
       },
       method: 'POST',
       json: true
      },
  function(err, r, body) {
   if (err) {
     ...
   }
   res.json({access_token: body.access_token, refresh_token: rt});
  });
 }
}

//
// Register Google Strategy in Passport
//
passport.use(new GoogleStrategy({
 clientID: config.client_id,
 clientSecret: config.client_secret,
 callbackURL: config.callback_url
}, function(accessToken, refreshToken, profile, done) {
 done(null, {accessToken: accessToken, refreshToken: refreshToken, profile: profile});
}));

module.exports = router;

Authorization

router.all('/*', ensureAuth);

router.put('/:scoreId/basket', ensureOwner, scoreBasket);

function ensureAuthenticated(req, res, next) {
 var reqAuth = req.headers.authorization;
 if (!reqAuth) {
  return res.send(401); // Unauthorized
 }

 var token = reqAuth.replace(/^\s*Bearer\s*/, '');
 
 if (!token) {
  retrun res.send(401); // Unauthorized
 }
 
 jwt.verify(token, jwtSecret, function(err, decode) {
  if (err) {
   return res.send(401);
  }
  req.user = decode.profile;
  req.token = token;
  next(null);
 });
}
function ensureOwner(req, res, next) {
 var scoreId = req.params.scoreId;
 var userId = req.user.id;
 if (!scoreId && !userId) {
  return res.status(500).send('Invalid user or score');
 }
 
 daoScore.getById(scoreId, function(err, score) {
  if (err) {
   return res.status(500).send('error');
  }
  
  if (!score) {
   return res.status(500).send('invalid score');
  }
  
  if (!score || score.owner !== userId) {
   return res.status(500).send('invalid owner');
  }
  next(null);
 });
}

MongoDB

Relational

NoSQL

Relational DB vs NoSQL DB

  • Focused on data integrity
  • SQL
  • Strict schemes
  • ACID transactions
  • Good for write
  • Independent data design
  • Focused on performance and scalability
  • Proprietary query language
  • Flexible schemes
  • Poor support to transantions
  • Good for read
  • The design data is dependent on usage

NoSQL !== Not SQL

NoSQL === Not Only SQL

What is MongoDB?

  • Created by 10Gen in 2007
  • Written in C++
  • Document oriented
  • Free schema
  • JSON (BSON) format
  • Powerful query engine
  • Powerful index support
  • Javascript dialect query lenguage
  • Multiple drivers for other languages
  • Sharding and replication support
  • Single document transaction support

Vocabulary : RDBMS vs MongoDB

RDBS MongoDB
Database Database
Table Collection
Record Document
Index Index
Partition Shard
Foreign key Reference

Document oriented

{
  "_id" : ObjectId("5037ee4a1084eb3ffeef7228"),
  "name" : "Peter",
  "age" : "Here's my blog post.",
  "bithdate" : ISODate("1982-08-24"),
  "friends" : [
    {
      "_id": ObjectId("5037ee4a1084eb3ffeef7238"),
      "name": "John"
    },
    {
      "_id" : ObjectId("5037ee4a1084eb3ffeef7245"),
      "name": "Anna"
    }
  ]
}

JavaScript query language 

> use personsDB
switched to db personsDB

> db.persons.insert({name: 'Anna', friends: []})


> db.persons.find()
{ "_id" : ObjectId("5037ee4a1084eb3ffeef7228"), "name": "Anna", "friends": [ ] }

MongoDB Native Driver

var MongoClient = require('mongodb').MongoClient;

// Connection URL
var url = 'mongodb://localhost:27017/test';

// Use connect method to connect to the Server
MongoClient.connect(url, function(err, db) {
  if (err) {
    throw err;
  }
  console.log("Connected correctly to server");

  // Get the documents collection
  var collection = db.collection('people');

  // Insert some documents
  collection.insert({name: 'Peter'}, function(err, result) {
    if (err) {
      throw err;
    }
    console.log("Inserted 1 document into the persons collection");
    
    // Find some documents
    collection.find({}).toArray(function(err, docs) {
      if (err) {
        throw err;
      }
      console.log("Found the following records");
      console.dir(docs);
      callback(null, docs);
    });
  });
});

Mongoskin

var mongo = require('mongoskin');

// Connection URL
var url = 'mongodb://localhost:27017/test';

var db = mongo.db(url, {native_parser:true});

db.bind('people').bind({
  create: function(name, callback) {
    this.insert({name: name}, callback);
  },
  getAll: function(callback) {
    this.find({}).toArray(callback);
  }
});

db.persons.create('Peter', function(err, newPerson) {
  if (err) {
    throw err;
  }
  console.log("Inserted 1 document into the persons collection");

  db.persons.getAll(function(err, docs) {
    if (err) {
      throw err;
    }
    console.log("Found the following records");
    console.dir(docs);
    callback(null, docs);
  });
});

Mongoose

var mongoose = require('mongoose');

// Connection URL
var url = 'mongodb://localhost:27017/test';

mongoose.connect(url);
var db = mongoose.connection;

db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function callback () {
	
});
  
var personSchema = mongoose.Schema({
  name: String
});

var Person = mongoose.model('Person', personSchema);

var person = new Person({name: 'Peter'});

person.save(function(err) {
  if (err) {
    throw err;
  }
  console.log("Inserted 1 document into the persons collection");

  Person.find({}).exec(function(err, docs) {
    if (err) {
      throw err;
    }
    console.log("Found the following records");
    console.dir(docs);
  });
});

Mongoose vs Native Driver

  • Mongoose is a ODM (Object <-> Document Mapper)
  • This is similar to an ORM in relational databases
  • While ORMs like Hibertane can make sense when we are working with relational databases, MongoDB is a NoSQL database
  • MongoDB documents are already quite similar to objects and Moogose only provides validation and behavior that can be easily implemented in JavaScript.
  • Therefore, Mongoose only makes sense if you do not want to know how MongoDB actually works.

Multi-tier Node App Architecture

//router tier
...
router.put('/:scoreId/basket', scoreBasket);

function scoreBasket(req, res, next) {
  updateScore(scoreManager.scoreBasket, req, res, next);
}
...
//managet tier
...
function scoreBasket(scoreId, team, points, callback) {
  var update = {
    $inc: {}
  };
  update.$inc[team] = points;
  if (checkValidBasket(points) && checkTeamName(team)) {
    daoScore.updateScore(scoreId, update, callback);
  } else {
    callback('Invalid points[' + points + '] or team[' + team + ']');
  }
}
//dao tier
...
var col = db.bind('score');
function updateScore(scoreId, update, callback) {
  var query = {
    id: toObjectID(scoreId)
  };
  var sort = [
    ['_id', 1]
  ];
  col.findAndModify(query, sort, update, {new: true}, callback);
}
...
col.bind({
  ...
  updateScore: updateScore
});
module.exports = col;

Exercise

  • Create a CRUD RestFul service for students and courses
  • The service should be allowed to create, retrieve, and remove courses and students
  • Students should be able to enroll in one or more courses
  • Courses can be open or closed
  • Closed courses do not admit more students
  • There must be a service that receives a student and returns all her courses
  • There will also be a service that receives one or more courses and it will return all students enrolled in all of them.

Node Fundamentals

By Javier Pérez

Node Fundamentals

  • 1,315