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