Marcus Young

Software. Microcontrollers. Beer.

Creating an MVC Blog With Node.js and MongoDB

| Comments

This is going to be my first big tutorial. This is a simple tutorial to show how to build the worlds most basic blog with comment system in Node.js with Express for the MVC Routing, MongoDB as the storage engine, and Jade as the html shorthand.

I’m going to assume you know how to install the base packages for Node.js, npm (node package manager), and mongodb. First, let’s set up the database to hold some articles. The mongo collection will be a basic object with an id, article_title, and article_body:

1
2
3
4
5
6
7
[myoung@F00 tutorial]$ mongo tutorial
MongoDB shell version: 2.0.2
connecting to: tutorial
> db.articles.remove()
> db.articles.insert({"article_title": "Article 1","article_body": "This would be the data from <strong>Article 1</strong>","article_date": new Date()});
> db.articles.insert({"article_title": "Article 2","article_body": "This would be the data from <strong>Article 2</strong>
I think I'll put some more data in this one =)","article_date": new Date()});

Now, let’s get the modules installed via npm.

1
2
3
4
5
[myoung@F00 ~]$ mkdir tutorial
[myoung@F00 ~]$ cd tutorial
[myoung@F00 tutorial]$ npm install connect # the middleware module for the model
[myoung@F00 tutorial]$ npm install express # the module to set up 'route' or controllers
[myoung@F00 tutorial]$ npm install jade # the module to set up shorthand html, or views

Next, the actual node app is needed. It’s a basic setup that loads the installed modules, listens on port 9095, and responds to any GET on ‘/’ with ‘Hello from node.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
var express = require('express')
var app = module.exports = express.createServer();
 
// Configuration
app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.static(__dirname + '/public'));
  app.set('view options', { layout: false });
  app.use(app.router);
});
 
app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
 
app.configure('production', function(){
  app.use(express.errorHandler());
});
 
// Routes
app.get('/',function(req,res) {
  res.writeHead(200);
  res.end("Hello from node.js!");
});
 
app.listen(9095);

If you run this, you’ll see that we have the worlds most basic server, let’s make it a little more advanced. This next version will set up a route that sends GET’s on ‘/’ to ‘views/index.jade’

1
2
3
app.get('/',function(req,res) {
  res.render('index', {});
});
1
2
3
4
5
6
7
-//new file: views/index.jade
!!!
html
  head
    title MVC Webpage
  body
    | This is the main layout

This is all fine and dandy, but it’s pretty basic. If we want to make any real web app, we have to minimize code reuse. The great thing about jade is its ability to extend other files.So let’s make a base layout.jade that holds all the html(in your final blog you’d want it to load the css, and actually set up the page). Extending in Jade works by creating a file, styling it with css, loading whatever jQuery modules or anything you’d want to display, and setting ‘blocks’. Blocks will be overridden by child pages, so you’d want it to be where your content is, or data that will change from view to view. A side note, which you’ll notice in later snippets, is that you can put inline JavaScript in the jade file, and it can determine what is rendered

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-//file is layout.jade...the 'parent' view
!!!
html
  head
    title MVC Webpage
  body
    block content
      | This is the main layout
 
-//index.jade
extends layout
-//now index is a 'child' of layout
block content
  | This was overridden by the index =)

Now that we’ve got a successful, notably basic, MVC framework set, let’s make it Mongo compatible so we can display some stuff. I tested a few Mongo modules, but enjoyed this one the most. It plays more like the CLI and has minimal overhead

1
[myoung@F00 tutorial]$ npm install mongodb

Now the app.js has to be modified to make the connection, and on response to ‘/’, grab the collection as an array, and pass it to the page as a variable.The index.jade also has to be modified to check for the array passed in, and loop through it displaying the title and body

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
var mongodb = require('mongodb')
  , Db = mongodb.Db
  , Server = mongodb.Server
  , db = new Db('tutorial', new Server('localhost', 27017, {auto_reconnect: true, native_parser: true}), {})
var express = require('express')
var app = module.exports = express.createServer();
 
// Configuration
app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.static(__dirname + '/public'));
  app.set('view options', { layout: false });
  app.use(app.router);
});
 
app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
 
app.configure('production', function(){
  app.use(express.errorHandler());
});
 
// Routes
//this is the route for /, or the root
app.get('/',function(req,res) {
  var data_collection = function(err,collection) {
    //find all articles in the collection, and sort by article_date...
    //pass it in as db_results
    collection.find().sort({article_date: -1}).toArray(function(err,db_results) {
      console.log(db_results);//just to show some output on the server, log the entire obj
      res.render('index', {//render the index
        blog_content: db_results //pass in the results to the page as a local blog_content
      });
    });
  };
 
  db.open(function(err, p_client) {
    db.collection('articles', data_collection);//this is the name of the collection...ie db.articles.find()
  });
 
});
 
app.listen(9095);
1
2
3
4
5
6
7
8
9
-//index.jade
extends layout
block content
  -if(locals.blog_content) {
    -for(var i=0; i<blog_content.length; i++) {
      h3=blog_content[i].article_title
      p !{blog_content[i].article_body}
    -}
  -}

At this point the page will loop through the blog articles in the database and display them all on the same page. To make it a little nicer, let’s do some creative modifications.Index.jade will take the length of the array, and if it’s 1, display that article with a blank ‘Comment Section’. If it’s more than 1, let’s loop through all of them, and make the title link to the article itself.For this to really do anything, we need the link to go to a distinct article, so App.js will have to be mofied to allow a direct link. In this case, We’re going to make it go to ‘/index/mongos_id_of_article’.You’ll notice a little work to get the id from the link. Node.js sees ‘/index/mongos_id_of_article’ as ‘/index/:article_id’, which is translated to ‘request.paracms.article_id’. The last part of this is the coersion from this id to a BSON id for a Mongo lookup (BSON.ObjectId).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-//index.jade
extends layout
block content
  -if(locals.blog_content) {
    -if(blog_content.length == 1) {
      h3=blog_content[0].article_title
      p !{blog_content[0].article_body}
      hr(width="100%")
      | Comment Section
    -} else {
      -for(var i=0; i<blog_content.length; i++) {
        h3
          a(href='/article/'+blog_content[i]._id)=blog_content[i].article_title
        p !{blog_content[i].article_body}
      -}
    -}
  -}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//this is the route for /article/somenumber .. somenumber is available in the request
app.get('/article/:article_id',function(req,res) {
  var data_collection = function(err,collection) {
    var BSON = mongodb.BSONPure;//load the BSON object
    var o_id = new BSON.ObjectID(req.params.article_id);//this is now a searchable
                                                        //id for collection
    collection.find({_id: o_id}).toArray(function(err,db_results) {
      res.render('index', {//render the index
        blog_content: db_results //pass the db_results to a local variable in index
                                 //called blog_content
      });
    });
  };
  db.open(function(err, p_client) {
    db.collection('articles', data_collection);//this is the name of the collection...ie db.articles.find()
  });
});

If you run it at this point, you’ll be able to hit the index, see links to articles and go directly to them with a blank comment section. Let’s make that comment section. To prep for this, we’ll need a form to POST to /comment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-//index.jade
extends layout
block content
  -if(locals.blog_content) {
    -if(blog_content.length == 1) {
      h3=blog_content[0].article_title
      p !{blog_content[0].article_body}
      hr(width="100%")
      | Comment Section
      form(method='post',action='/comment')
        label Name:
          input(type='text',name='postername')
        label Comment:
          textarea(type='text',name='postercomment')
        input(type='hidden',name='current_id',value=blog_content[0]._id)
        input(type='submit',value='submit')
    -} else {
      -for(var i=0; i<blog_content.length; i++) {
        h3
          a(href='/article/'+blog_content[i]._id)=blog_content[i].article_title
        p !{blog_content[i].article_body}
      -}
    -}
  -}

If you run this now, you’ll get an error. That’s because we don’t have a route for a POST to /comment, so let’s set that up.

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
app.post('/comment',function(req,res) {                                                                         
  var bodyarr = [];                                                                                             
  req.on('data', function(chunk){                                                                               
    bodyarr.push(chunk);                                                                                        
  });                                                                                                           
  req.on('end', function(){
    /* This is hackish but is to only show proof of concept                                                     
       This will split the bodyarr that we've created                                                           
          ex: id=4&body=what%20up&something=somethingelse                                                       
    */                                                                                                          
    var values = bodyarr.join('').split('&');                                                                   
    var BSON = mongodb.BSONPure;//this will load the mongo BSON                                                 
    var article_id = new BSON.ObjectID(values[2].split('=')[1]);//this was the argument for ID                  
 
    //create a 'comment' object that contains the poster and comment                                            
    var comment_object = {                                                                                      
      'poster': values[].split('=')[1],
      'comment': values[1].split('=')[1]
    };                                                                                                          
    var data_collection = function(err,collection) {                                                            
      //call update on the collection, find by _id, and push the comment object onto it                         
      collection.update({_id: article_id},{$push : { comments : comment_object }});
      res.redirect('back');
    };
    db.open(function(err, p_client) {                                                                           
      db.collection('articles', data_collection);//this is the name of the collection...ie db.articles.find()   
    });
  });
});

It got a little complicated in that snippet, so here’s a slight breakdown. The post takes the data as a stream (req.on) and pushes it to an array. On the end of the stream we’ll break up the values,and push it onto the mongo collection for that id (we know the id from the hidden form element). The push is an Object that’s a ‘poster’ and a ‘comment’. When this is finished, we’ll redirect backwards. This is dandy, but you’ll notice we haven’t set up the view to display comments, only store. The newest code has the addition of a loop. If comments exists(if the collection even has a comment array), we will loop through it and display it. For handiness, you’ll also notice the addition of a link to ‘/’

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
-//index.jade 
extends layout
-//overwrite the content block with new data
block content 
  -if(locals.blog_content) {//make sure it has a blog_content (to be safe and not
                            //cause an Express crash
    -if(blog_content.length == 1) {//only 1 article, means they clicked on
                                   //an article link to view it....
                                   //so render it with the comment form data
      h3=blog_content[0].article_title
      p !{blog_content[0].article_body}
      hr(width="100%")
      | Comment Section
      form(method='post',action='/comment')
        label Name:
          input(type='text',name='postername')
        label Comment:
          textarea(type='text',name='postercomment')
        input(type='hidden',name='current_id',value=blog_content[0]._id)
        input(type='submit',value='submit')
      hr(width="100%")
      -var comments = blog_content[0].comments;
      -if(comments && comments != undefined) {
        -for(var i=0; i<comments.length;i++) {
          h3=comments[i].poster
          | !{comments[i].comment}
        -}
      -}
      hr(width="100%")
      a(href='/') Home
    -} else {//more than one article, meaning they're at the 'blog root',
             //looking at all the links
      -for(var i=0; i<blog_content.length; i++) {//loop through blog array
        h3
          a(href='/article/'+blog_content[i]._id)=blog_content[i].article_title
        p !{blog_content[i].article_body}
      -}
    -}
  -}

This last snippet is a clean up of the comment post. Since it’s parsed as parameters, spaces are +’s. This could be fixed by modifying the way I handle the post as a stream, but for demonstrations sake, let’s just do a regex replace of /+/ to spaces.

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
app.post('/comment',function(req,res) {
  var bodyarr = [];
  req.on('data', function(chunk){
    bodyarr.push(chunk);
  });
  req.on('end', function(){//the data has finished coming in
    /* This is hackish but is to only show proof of concept
       This will split the bodyarr that we've created
          ex: id=4&body=what%20up&something=somethingelse
    */
    var values = bodyarr.join('').split('&');
    var BSON = mongodb.BSONPure;//this will load the mongo BSON
    var article_id = new BSON.ObjectID(values[2].split('=')[1]);//this was the argument for ID
 
    //create a 'comment' object that contains the poster and comment
    var comment_object = {
      'poster': values[].split('=')[1].replace(/+/g,' '),//had +'s for spaces, so replace them
      'comment': values[1].split('=')[1].replace(/+/g,' ')
    };
 
    var data_collection = function(err,collection) {
      //call update on the collection, find by _id, and push the comment object onto it
      collection.update({_id: article_id},{$push : { comments : comment_object }});
      res.redirect('back');//send the user to the previous page
    };
    db.open(function(err, p_client) {
      db.collection('articles', data_collection);//this is the name of the collection...ie db.articles.find()
    });
  });
});

Comments