Stellar Chariot

By Arun Neelakandan

Express and Node.js for Djangonauts: A Tutorial on Building a Polls App, Part 2

This part continues from where we left off in part 1.

This part will cover part 3 of the Django tutorial. Why? Because part 2 of the Django tutorial covers the admin interface Django is able to generate for you. Express has no equivalent tool that is as comprehensive or as feature complete as Django’s admin interface. There are some projects addressing this issue, but they’re still seedlings: http://stackoverflow.com/a/11965061/977931

A general note before we begin

You may need to restart the Node app/server after making changes to code. Just stop the server if it’s already running (using Ctrl + C) and then start it up again (e.g. using node app).

The same context and prerequisites apply from the first part of the tutorial.

Code on Github

You can find the code used in this tutorial on Github under the ‘part-2’ tag:

View Code for Part 2 on Github

Writing our first ‘view’

Open the app.js file and add the following code just prior to the http.createServer call at the end:

app.js
1
2
3
function pollIndex(req, res) {
  res.send("Hello, world. You're at the poll index.");
}

This is the simplest ‘view’ possible in Express. Views in Express are conceptually similar to Django views — they’re both functions that take in a request (req) and return a response (res). For convenience, I’ll call such functions a ‘view’ as well, even though it’s not entirely accurate in Node.js/JavaScript/Express parlance.

To call this Express view, we need to map it to a URL, which we can do using get(). Add this code after the function pollIndex() (that we just added):

app.js
1
app.get('/polls', pollIndex);

This code defines that whenever a HTTP GET request is received at the location ‘/polls’, let the view pollIndex() deal with it. You can also use the different HTTP verbs to route requests e.g. app.post('/someplace', someView);, but I’m getting ahead of myself.

You can find more information on routing (including using regexes to capture URL bits as in Django) in the Express API reference for application routing.

Now, if you save this file and run node app and visit http://localhost:3000/polls in your browser, you should see the message “Hello, world. You’re at the poll index.”

Writing more views

Let’s add more views to app.js! These views are different because they use a parameter from the request:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function detail(req, res) {
  poll_id = req.params.poll_id
  res.send("You're looking at poll " + poll);
}

function results(req, res) {
  poll_id = req.params.poll_id
  res.send("You're looking at the results of poll " + poll_id);
}

function vote(req, res) {
  poll_id = req.params.poll_id
  res.send("You're voting on poll " +  poll_id);
}

In Django, these parameters would be passed into the view as an argument. For example:

views.py
1
2
3
4
5
6
7
8
def detail(request, poll_id):
    return HttpResponse("You're looking at poll %s." % poll_id)

def results(request, poll_id):
    return HttpResponse("You're looking at the results of poll %s." % poll_id)

def vote(request, poll_id):
    return HttpResponse("You're voting on poll %s." % poll_id)

Anyhoo, let’s update our routes in app.js from this:

app.js
1
app.get('/polls', pollIndex);

to this:

app.js
1
2
3
4
5
app.get('/polls', pollIndex);
app.get('/polls/:poll_id', detail);
app.get('/polls/:poll_id/results', results);
app.get('/polls/:poll_id/vote', vote);
app.post('/polls/:poll_id/vote', vote);

Load /polls/22 in your browser and it’ll call the detail() function and display whatever ID you provide in the URL. Similarly, try /polls/22/results and /polls/22/vote too, which will bring up the placeholder results and voting pages.

Note also the explicit post specification in the routes for voting via the app.post() invocation, which will eventually be used for capturing form submissions. In Django, the view typically deals with POST submissions (unless some Django middleware intercepts it). Express gives you a finer control at the routing level about directing HTTP requests to different views.

The URLs/routes in the config are loaded in order, just as in Django. That is, ‘/polls’ would be tried before ‘/polls/44/results’.

You can also use regular expressions if you need or choose to. Consider an example that shows shoes within a certain size range:

app.js
1
2
3
4
5
6
7
8
// Show shoes between certain sizes
// Example valid URL 1: /items/shoes/sizes/8-13
// Example valid URL 2: /items/shoes/sizes/10-16
app.get(/^\/items\/shoes\/sizes\/(\d{0,2})-(\d{1,2})$/, function(req, res){
  var min_size = req.params[0];
  var max_size = req.params[1];
  res.send('Showing shoes between sizes ' + min_size + ' to ' + max_size);
});

Write views that actually do something

A view is supposed to returned a response given the request. Once we reach the view level, what gets done is up to you. Like in Django, you can pull stuff out of a database, initiate rocket launchers, render a CSV file — whatever. Incidentally, I wonder if there is a web interface for nuke codes that the President of the United States controls…

Now, let’s grab the latest 5 polls from the database, separated by commas, according to the publication date:

1. Ensure var pollsdb = require('./models/db') and mongoose = require('mongoose') are in the dependencies near the top of app.js. If they’re not there, add them. So it might look something like this:

app.js
1
2
3
4
5
6
7
8
9
10
11
/**
 * Module dependencies.
 */

var express = require('express')
  , routes = require('./routes')
  , user = require('./routes/user')
  , http = require('http')
  , mongoose = require('mongoose')
  , pollsdb = require('./models/db')
  , path = require('path');

2. Subsequent to (i.e. after) the above, add the following to app.js:

app.js
1
2
3
4
5
6
7
8
// Configure appropriately
mongoose.connect('mongodb://localhost/pollsdb');

var conn = mongoose.connection
conn.on('error', console.error.bind(console, 'Database connection error:'));
conn.once('open', function callback () { console.log("Hey handsome. Database connected.") })

var Poll = conn.model("Poll", pollsdb.pollSchema)

3. Update the pollIndex() view:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function pollIndex(req, res) {
  Poll.find().sort('-pubDate').limit(5).exec(function(err, polls) {
    if (err)
      // Pass it along to the next error-handling middleware to deal with
      next("Uh oh! Something went wrong when fetching polls from the database.");
    else if (polls) {
      console.log(polls);
      var questions = new Array();
      for (var i = 0; i < polls.length; i++) {
        questions.push(polls[i].question);
      }
      // Make a comma-separated string of questions
      result = questions.join(", ");
      console.log(result);
      res.send(result);
    }
    else {
      res.send("No polls found :(");
    }
  });
}

Firing up http://localhost:3000/polls in your browser should give you a comma-separated list of the last 5 questions.

Introducting templates

The result string is hardcoded into the response, which is no good! Let’s use templates instead to make things more maintainable.

By default, Jade is used for templating. You can use different engines like Haml or EJS if you’d like. When creating an Express skeleton project, you can choose from pool of view engines (like EJS) to get started quickly. Run express --help for more information.

And if you’re feeling a bit ‘homesick’, there are also some Jinja implementations for Node: Swig, Jinja-like JS templating language. I haven’t used these, so your mileage may vary.

The directory where templates are stored and template engine used is specified in app.js with:

app.js
1
2
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');

Anyway, create a file called pollIndex.jade in the views folder and put the following code:

pollIndex.jade
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//- Example of template inheritance
//- A default layout (layout.jade) was created with the skeleton project
extends layout

block content
  h1= title
  if latest_poll_list
    ul
      //- Loop through the list of polls.
      //- Akin to the for template tag in the Django templating language.
      each poll in latest_poll_list
        li
          //- Jade makes it possible to write inline JavaScript code in templates.
          //- Anything after an equals sign ('=') is 'buffered code'
          //- which outputs the result of evaluating the JavaScript expression.
          //- Refer to the Jade docs: http://jade-lang.com/reference/
          //- 
          //- Here, we create a link to the poll.
          //- Note the dot notation used, similar to Django.
          a(href='/polls/' + poll._id)= poll.question
  else
    p No Polls Found :(

and update the res.send(result); line in the pollIndex() view to:

app.js
1
res.render('pollIndex', { title: "Latest Polls", latest_poll_list: polls });

Crank up http://localhost:3000/polls in your browser and you should see a list of polls.

A list of latest polls

The code to render and return a template is alike to Django. Here’s how you might do it in Django:

views.py
1
return render(request, 'polls/index.html', {'latest_poll_list': latest_poll_list})

The detail() view

Let’s work on the poll detail view, which shows the poll question. Update the detail() view as follows:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function detail(req, res, next) {
  var poll_id = req.params.poll_id;
  // findById() is akin to Django's get(pk=poll_id)
  Poll.findById(poll_id).exec(function(err, poll) {
    if (err) {
      // Pass it along to the next error-handling middleware to deal with.
      // It should return a 500 Internal Server Error
      next("Uh oh! Something went wrong when fetching a poll from the database.");
    }
    else if (poll) {
      res.render('pollDetail', { poll: poll });
    }
    else {
      // Pass it along to the next middleware component
      // as there was nothing found (should deliver a 404)
      next();
    }
  });
}

Also create a simple template pollDetail.jade for the time being:

404.jade
1
2
3
4
5
6
7
8
9
10
11
12
13
//- Example of template inheritance
//- A default layout (layout.jade) was created with the skeleton project
extends layout

block content
  if poll
    h1= "Poll: " + poll.question
    ul
    each choice in poll.choices
      //- Equivalent to `li= choice.choiceText`
      li #{choice.choiceText}
  else
    p No poll found :(
An example poll detail page

Raising 404 errors

To send a 404 status back, you simply set the HTTP in the response e.g. res.status(404). Here’s what the Express documentation has to say about handling 404s (from the FAQ):

In Express 404s are not the result of an error, thus the error-handler middleware will not capture 404s, this is because a 404 is simply the absence of additional work to do, in other words Express has executed all middleware / routes and found that none of them responded. All you need to do is add a middleware at the very bottom below all the others to handle a 404: app.use(function(req, res, next){ res.send(404, ‘Sorry cant find that!’); });

So, based on the above advice, let’s add a 404 handler thingy — but one that’s fancier and has the site’s look-and-feel.

Add this code somewhere after the routing (app.use(app.router);) and static files (app.use(express.static(path.join(__dirname, 'public')));) middleware usage declarations:

app.js
1
2
3
4
5
// Error Handling
// As per "How do you handle 404s?" on http://expressjs.com/faq.html
app.use(function(req, res){
  res.status(404).render('404');
});

and create this template file 404.jade in the views folder:

404.jade
1
2
3
4
5
extends layout

block content
  h1 404: Page Not Found
  p Sorry, can't find the page you're after :(

If you now visit a non-existent page like http://localhost:3000/polls/boogboo, you should see a nice 404 page (unless you have a poll with the ID of ‘boogaboo’!):

An example page not found page

Unlike Django where you say “I tried to find this poll but couldn’t, so have a 404 bro,” you say “I tried to find this poll but couldn’t, so I’ll pass it on to the next middleware dude — he’ll deal with it.”

Removing hardcoded URLs in templates

In our pollIndex.jade template, we have hardcoded the URL like so:

404.jade
1
a(href='/polls/' + poll._id)= poll.question

In Django, you can use the {&#123; url &#125;} template tag to decouple URLs. There is no counterpart to this Express by default, however you can implement something close to it. But cool URIs shouldn’t change and redirects should be installed if they do.

Neatening things up

By now, you should’ve noticed how cluttered and full app.js is getting. Let’s chunk things things up a bit, shall we?

Let’s move the views into its own file. I’ve decided to put these views into the routes/index.js file (which was generated with the app skeleton was generated). This effectively becomes a views.py equivalent:

routes/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
var mongoose = require('mongoose');
var pollsdb = require('../models/db');

mongoose.connect('mongodb://localhost/pollsdb');

var conn = mongoose.connection
conn.on('error', console.error.bind(console, 'Database connection error:'));
conn.once('open', function callback () { console.log("Hey handsome. Database connected.") })

var Poll = conn.model("Poll", pollsdb.pollSchema)


/*
 * GET home page.
 */

exports.index = function(req, res){
  res.render('index', { title: 'Express' });
};

exports.pollIndex = function(req, res, next) {
  Poll.find().sort('-pubDate').limit(5).exec(function(err, polls) {
    if (err)
      // Pass it along to the next error-handling middleware to deal with
      next("Uh oh! Something went wrong when fetching polls from the database.");
    else {
      var questions = new Array();
      for (var i = 0; i < polls.length; i++) {
        questions.push(polls[i].question);
      }
      // Make a comma-separated string of questions
      result = questions.join(", ");
      //res.send(result);
      res.render('pollIndex', { title: "Latest Polls", latest_poll_list: polls });
    }
  });
}

exports.detail = function(req, res, next) {
  var poll_id = req.params.poll_id;
  // Check if the ID provided is an ObjectID used by MongoDB (used by default)
  // Could also do this matching in the routes specification
  // Refer to: http://stackoverflow.com/a/14942113/977931
  if (poll_id.match(/^[0-9a-fA-F]{24}$/)) {
    // findById() is akin to Django's get(pk=poll_id)
    Poll.findById(poll_id).exec(function(err, poll) {
      if (err) {
        console.log(err);
        // Return a 500 Internal Server Error
        next("Uh oh! Something went wrong when fetching a poll from the database.");
      }
      else if (poll) {
        res.render('pollDetail', { poll: poll });
      }
      else {
        // Pass it along, nothing found (should deliver a 404)
        next();
      }
    });
  }
  else
    // Pass it along, nothing found (should deliver a 404)
    next();
}

exports.results = function(req, res) {
    poll_id = req.params.poll_id
    res.send("You're looking at the results of poll " + poll_id);
}

exports.vote = function(req, res) {
    poll_id = req.params.poll_id
    res.send("You're voting on poll " +  poll_id);
}

Note how I’ve moved the relevant database connection configuration into the top of routes/index.js.

The app.js file would look like this now:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
 * Module dependencies.
 */

var express = require('express')
  , routes = require('./routes')
  , user = require('./routes/user')
  , http = require('http')
  , mongoose = require('mongoose')
  , pollsdb = require('./models/db')
  , path = require('path');

var app = express();

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
// Error Handling
// As per "How do you handle 404s?" on http://expressjs.com/faq.html
app.use(function(req, res){
  res.status(404).render('404');
});

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

app.get('/', routes.index);
app.get('/users', user.list);

app.get('/polls', routes.pollIndex);
app.get('/polls/:poll_id', routes.detail);
app.get('/polls/:poll_id/results', routes.results);
app.get('/polls/:poll_id/vote', routes.vote);
app.post('/polls/:poll_id/vote', routes.vote);

// Show shoes between certain sizes
// Example valid URL 1: /items/shoes/sizes/8-13
// Example valid URL 2: /items/shoes/sizes/10-13
app.get(/^\/items\/shoes\/sizes\/(\d{0,2})-(\d{1,2})$/, function(req, res){
  var min_size = req.params[0];
  var max_size = req.params[1];
  res.send('Showing shoes between sizes ' + min_size + ' to ' + max_size);
});

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

View the full source on Github

You can check out the final source code at this stage on Github under the ‘part-2’ tag:

View Code for Part 2 on Github

To be continued

This brings part 2 of the tutorial to an end. Stay tuned for the next part which will deal with simple form processing. Follow me on Twitter or subscribe to the RSS feed for updates.

If you have any questions, leave them in the comments below!

Comments