JavaScript technologies

Node.js, npm and express.js

Horváth, Győző
senior lecturer
horvath.gyozo@inf.elte.hu

Financed from the financial support ELTE won from the Higher Education Restructuring Fund of the Hungarian Government

Node.js és NPM

Node.js

  • command-line JavaScript
  • based on the Google V8 JavaScript engine
  • platform
  • Asynchronous processing
  • node command (interactive shell)
  • node filename (executing a file)

npm

  • Node Package Manager
  • npm command
  • node_modules directory
  • npm init: creating a project description file (package.json)
  • npm install package: installing package locally
  • npm install -g package: installing package for global access
  • npm install package --save: installing and saving a package as a dependency to package.json
  • npm install package --save-dev: installing and saving a package as a development dependency (e.g. test runner)

Node.js module system

  • main program + modules
  • CommonJS module format
  • one module = one file
  • require: imports a module
  • module.exports: exports functionality
//Modul: math.js
function add(a, b) {
    return a + b;
}

module.exports = add;
//Main program: index.js
var add = require('./math');

console.log(add(2, 3));

Exporting multiple entities

// math.js
module.exports = {
    add:      function (a, b) { return a + b; },
    multiply: function (a, b) { return a * b; },
};
// index.js
var math = require('./math');

console.log(math.add(2, 3));

// OR
var multiply = require('./math').multiply;

console.log(multiply(2, 3));

Node as a web server

http module

var http = require('http');

var port = process.env.PORT || 3000;
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello browser!\n');
}).listen(port);

From http module to express

A road from http module to express...

Minimal express application

var express = require('express');
var app = express();

var port = process.env.PORT || 3000;
app.listen(port);

Callback on startup

var express = require('express');
var app = express();

var port = process.env.PORT || 3000;
var server = app.listen(port, function () {
  console.log('Server is running!');
});

Express with http module

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

var app = express();

var port = process.env.PORT || 3000;
http.createServer(app).listen(port);

Express basic concepts

  • HTTP request: request object
    • URI
    • HTTP method
    • (data)
    • headers
  • --> HTTP response: response object
    • status
    • mime-type
    • content
    • headers
  • processing: middlewares and route handlers

Middleware

Middleware is a function, which has access to the

  • request object (request)
  • response object (response)
  • next middleware (next)
function (request, response, next) {

}

Middleware

  • Can run any code
  • Can change the request and response object
  • Can close the request-response process
  • Can call the next middleware
function (request, response, next) {
    //arbitrary code
    //reading/writing the request, response object
    next();
}

Using the middlewares

//General form
app.use([path], middleware [, middleware]);

//Hit by any route
app.use(function (req, res, next) {
    console.log('Middleware');
    next();
});

//Run on every route starting with '/users'
app.use('/users', function (req, res, next) { /*...*/ });

//Example endpoint
app.use(function (req, res, next) {
    console.log('Endpoint');
});

Express application

The express application is a series of middlewares ending with an endpoint:

middleware
middleware-->middleware
middleware
middleware
middleware-->endpoint

Routing

Assigning an URI endpoint to a route handler (controller).

URIs with endpoints:

http://expressjs.com/guide/routing.html
http://localhost:8080/snippets/3

Route

  • URI (path)
  • HTTP method (METHOD)
  • one or more route handler (handler)
//Generally
app.METHOD(path, handler [, handler ...]);
app.METHOD(path [, middleware, ...] handler);

//For example
app.get('/apple', function (req, res) {
    //code
});

Route methods

  • get
  • post
  • put
  • delete
  • options
  • etc.
  • all

Full list of route methods

Route paths

//szöveg
app.get('/apple', function (req, res) { /* ... */ });
app.get('/public/index.html', function (req, res) { /* ... */ });

//szövegminta
app.get('/ab?cd', ...);     //acd or abcd
app.get('/ab+cd', ...);     //abcd, abbcd, abbbcd, ...
app.get('/ab*cd', ...);     //abcd, ab1cd, abSOMETHINGcd
app.get('/ab(cd)?e', ...);  //abe, abcde

//reguláris kifejezés
app.get(/apple/, ...);       //apple
app.get(/.*apple$/, ...);    //apple, chapple, redapple

Route handlers

  • Arbitrary middleware
  • Without calling next() (endpoint)
app.get('/', function (req, res) { /*...*/ });
app.get('/', function (req, res, next) {
    console.log('middleware');
    next();
}, function (req, res) {
    console.log('end');
})
app.get('/', middleware1, middleware2, middleware3);
app.get('/', [middleware1, middleware2, middleware3]);

Types of middlewares

//Application-level
app.use(...);

//Router-level
var router = express.Router();
router.use(...)

//Endpoint-level
app.get('/apple', middleware1, middleware2, middleware3);
app.use('/apple', middleware1, middleware2, middleware3);
router.get('/apple', middleware1, middleware2, middleware3);

//error handler
app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Server side error!');
});

//plugin
app.use(express.static('public'));
app.use(cookieParser());

route() method

app.get ('/apple', function (req, res) { /*...*/ });
app.post('/apple', function (req, res) { /*...*/ });
app.put ('/apple', function (req, res) { /*...*/ });

//the above code can be written in this way:

app.route('/apple')
    .get (function (req, res) { /*...*/ })
    .post(function (req, res) { /*...*/ })
    .put (function (req, res) { /*...*/ });

Router

  • Modularized route handling
  • Isolated routes
  • full-featured middleware and endpoint handling
  • kind of mini-application
  • or: app is extended with Router functionality
  • Router is a middleware

Defining and using the Router

//apple.js
//Definition
var express = require('express');
var router = express.Router();

// middleware only for this router
router.use(function (req, res, next) { /*...*/ next(); });

// endpoints only for this router
router.get('/', function(req, res) { /*...*/ });
router.get('/apple', function(req, res) { /*...*/ });

module.exports = router;
//Usage
var apple = require('./apple')

// /public and /public/apple 
app.use('/public', apple);

The request object

  • req.path
  • req.ip
  • req.get()
  • req.params
  • req.body

The response object

  • res.download(): downloading a file
  • res.end(): end of response
  • res.json(): JSON answer
  • res.jsonp(): JSON answer in JSONP format
  • res.redirect(): redirection
  • res.render(): displaying a view template
  • res.send(): different kind of answers
  • res.sendFile(): reading and outputing a file

Response documentation

Node.js vs PHP

Application server concepts

Serving static content

express.static middleware

  • express.static(root [, options])
  • Description
  • Accessing public/apple.html file: /apple.html
app.use(express.static('public'));

Page templates

Setting up a template engine

  • ejs
  • jade
  • hbs
  • hogan.js
  • dust.js
app.set('views', './views');
app.set('view engine', 'ejs')

EJS

EJS (Embedded JavaScript)

The template

<% code %>
<%= value, helper %>
<%- raw (unescaped) HTML %>
Hello <%= name %>!

Displaying a template (rendering)

// Rendering a view
var express = require('express');
var app = express();

// view engine setup
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');

app.get('/main', function (req, res) {
    res.render('main', {
        name: 'Győző'
    });
});

Layout 1.

EJS include

<!-- including the content of header.ejs and footer.ejs -->
<% include header %>
Hello <%= name %>!
<% include footer %>

Layout 2.

express-partials module

var express = require('express');
var partials = require('express-partials');
var app = express();

// view engine setup
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(partials());

app.get('/main', function (req, res) {
    res.render('main');
});

Layout 2.

layout.ejs

<!DOCTYPE html><title>EJS</title>
<h1>Partials</h1>
<hr>
<div>
    <%- body %>
</div>
<hr>
Footer

main.ejs

Hello <%= name %>!

Processing input

Input data from the client

  • URI parameters
  • query part of the URI (GET parameters)
  • HTTP message body

URI parameters

req.params.parameterName

// GET /user/adam
app.get('/user/:name', function (req, res) {
    var name = req.params.name; // => adam
    //...
});

URI parameters middleware

//Parameter middleware
router.param('name', function (req, res, next, name, paramName) {
    console.log('paramName' + ': ' + name);
    next();
})

app.get('/user/:name', function (req, res) {
    var name = req.params.name; 
    //...
});

GET parameters

req.query.parameterName

// GET /users?id=42&ref=45428174v24t2
app.get('/users', function (req, res) {
    console.log(req.query);
    var id = req.query.id;
    var ref = req.query.ref;
})

POST parameters

req.body.parameterName

npm install body-parser --save

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

//...

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

//...

// POST /users
app.post('/users', function(req, res) {
    var firstName = req.body.firstname;
    var lastName = req.body.lastname;
    //...
});

Form processing

POST-REDIRECT-GET method

  1. Displaying the form with GET method
  2. Sending data with POST method
  3. In case of success redirection to success page
  4. GET success page

Form processing: errors 1.

  1. Displaying the form with GET method
  2. Sending data with POST method
  3. In case of error show the form again with error messages and prefilled values
  4. Sending data with POST method
  5. In case of success redirection to success page
  6. GET success page

Form processing: errors 2.

  1. Displaying the form with GET method
  2. Sending data with POST method
  3. In case of error redirect to step 1.; error messages and sent parameters must be stored between the two requests (session).
  4. Sending data with POST method
  5. In case of success redirection to success page
  6. GET success page

Validating sent values

express-validator module

var expressValidator = require('express-validator');

app.use(express.bodyParser());
app.use(expressValidator());

app.post('/', function(req, res) {

    req.checkBody('age', 'Invalid age').notEmpty().isInt();

    var errors = req.validationErrors();
    if (errors) {
        //error handling
    } else {
        //success branch
    }
});

Session handling

Setting up

Installation (express-session)

npm install express-session --save

Usage

var session = require('express-session');

app.use(session({
    cookie: { maxAge: 60000 },
    secret: 'secret code',
    resave: false,
    saveUninitialized: false,
}));

app.get('/', function (req, res) {
    // Read
    req.session.parameter
    // Write
    req.session.parameter = value;
});

Counter example

app.get('/', function (req, res) {
    var sess = req.session;
    var cnt = 0;

    if (sess.cnt) {
        cnt = sess.cnt;
    }
    cnt++;
    sess.cnt = cnt;

    res.render('counter', {
        cnt: cnt
    });
});

Flash data

  • Data living in the session only for one request long.
  • Typically for passing messages.
  • connect-flash module
npm install connect-flash --save
var flash = require('connect-flash');

app.use(flash());

app.get('/', function (req, res) {
    // Read
    req.flash(type);
    // Write
    req.flash(type, msg);
});

Persisting state to files

Native file operations

fs modul

  • Asynchronous
    • fs.readFile()
    • fs.writeFile()
  • Synchronous
    • fs.readFileSync()
    • fs.writeFileSync()

NeDB

Github page of the project

npm install --save nedb

Authentication

Passport.js

  • Documentation
  • Very simple interface
  • Authentication strategies
    • local
    • Facebook
    • Google
    • Twitter
    • Github
    • etc

Installing and importing

npm install passport --save
var passport = require('passport');

Setting up a strategy

Installing

npm install passport-<strategy> --save

Using

var Strategy = require('passport-<strategy>');

passport.use([name, ] new Strategy([options, ]
    function(username, password, done) {
        // Validating username and password

        // Returning the result
        return done(err); //Error
        return done(null, false, { message: 'Üzenet' }); //Authentication failed
        return done(null, user); //Successful authentication, giving back the uesr object
    }
));

Registering middlewares

//Passport middlewares
app.use(passport.initialize());

//Using sessions (optional)
app.use(passport.session());

Passport and the session

Deciding what to store in the session

// Serialization to the session
passport.serializeUser(function(user, done) {
    done(null, <data stored in the session>);
});

// Recovery from the session
passport.deserializeUser(function(obj, done) {
    done(null, <the expected user object>);
});

Using the authentication

passport.authenticate() method on endpoints

// Redirection with parameters
app.post('/signup', passport.authenticate(<strategy>, {
    successRedirect:    '/login',
    failureRedirect:    '/login/signup',
    failureFlash:       true, // req.flash() method is needed!
    failureMessage:     "Bad credentials", // optional default message
    badRequestMessage:  'Missing credentials', // missing authentication data message
}));

// With callback function
app.post('/login', passport.authenticate(<strategy>),
    function(req, res) {
        // To be called on successful authentication
        // `req.user` is the authenticated user
        res.redirect('/users/' + req.user.username);
    });

Logging out

req.logout()

Authorization

Access management

What kind of resources an authenticated user can access?

Multiple level:

  • application
  • endpoint
  • part of the page
  • input field

Grouping users: roles

Solutions

Typically on endpoint middlewares.

  • Passport
    • req.isAuthenticated()
  • Module
    • connect-ensure-login module
  • High-level
    • acl module

Login form example

The form

GET /login

<!doctype html><title>Login</title>
<form action="/login" method="post">
    <div>
        <label>Username:</label>
        <input type="text" name="username"/>
    </div>
    <div>
        <label>Password:</label>
        <input type="password" name="password"/>
    </div>
    <div>
        <input type="submit" value="Log In"/>
    </div>
</form>

Login form example

Server side logic: using in-memory credentials

var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var credentials = {
    'user1': 'user1',
    'a': 'a',
    'user3': 'user3',
};

passport.use('local', new LocalStrategy(
    function(username, password, done) {
        // console.log(username, password);
        if (!credentials[username]) {
         return done(null, false, { message: 'Incorrect username.' });
        }
        if (credentials[username] !== password) {
         return done(null, false, { message: 'Incorrect password.' });
        }
        var user = {
         id: username,
         username: username
        };
        return done(null, user);
    }
));

passport.serializeUser(function(user, done) {
   done(null, user.id);
});
 
passport.deserializeUser(function(id, done) {
   var user = credentials[id];
   done(null, user);
});

// route middleware to make sure
function isLoggedIn(req, res, next) {
    // if user is authenticated in the session, carry on
    if (req.isAuthenticated())
        return next();

    // if they aren't redirect them to the home page
    res.redirect('/login');
}
        
app.use(passport.initialize());
app.use(passport.session());

app.get('/vedett', isLoggedIn, function (req, res) {
    res.render('vedett');
});

app.get('/login', function (req, res) {
    res.render('login');
})

app.post('/login', 
    passport.authenticate('local', { successRedirect: '/vedett',
                                   failureRedirect: '/login' })
);

Login form example

Server side logic: using file

var Datastore = require('nedb'),
    db = new Datastore({ filename: 'credentials.nedb', autoload: true });

passport.use('local', new LocalStrategy(
    function(username, password, done) {
        db.find({username: username}, function (err, docs) {
            if (docs.length === 0) {
                return done(null, false, { message: 'Incorrect username.' });
            }
            var user = docs[0];
            if (user.password !== password) {
                console.log(1);
                return done(null, false, { message: 'Incorrect password.' });
            }

            return done(null, user);
        });
   }
));

passport.serializeUser(function(user, done) {
   done(null, user._id);
});
 
passport.deserializeUser(function(id, done) {
   db.findOne({_id: id}, function (err, user) {
        done(null, user);
   });
});

//Registering two default users
app.get('/fill', function (req,res) {
    db.insert({
        username: 'a',
        password: 'a'
    });
    db.insert({
        username: 'b',
        password: 'b'
    });
    res.end();
})