NodeJS Tutorial with CouchDB and Haml – ErdNodeFlips

The aim of this tutorial is to create a Node powered web app using a CouchDB database and Haml based templates. I wanted something relatively simple that I could do in a couple of sittings, but complex enough to be of some actual use. In the end I opted for a “top 10 list” type application. The functionality being as follows:

  1. To list a number of items
  2. To remove an item from the list
  3. To add an item to the list

Obviously, before getting cracking with this tutorial I had to think up a name for this “killer app” of the Node world.

So I give you ErdNodeFlips! In honour of erdnussflips – the strangely addictive Wotsit type things that taste of peanut butter and stick to the top of your mouth. Yum! (OMG – they even have their own Facebook page!)

All code is available on GitHub.

Directory Structure and Libraries

The first step is to create a directory structure for the application and bring in the relevant libraries. We need two directories underneath the top-level application root: templates (which will hold the haml files) and libs (which will contain 3rd party libraries). We are going to grab two libraries off GitHub:

$ cd /
$ mkdir {libs,templates}
$ cd libs
$ git clone git://github.com/felixge/node-couchdb.git
$ git clone http://github.com/creationix/haml-js.git
$ cd ../
Please help improve this blog and take a quick survey. Thanks!

Basic Web Server

Now we can get started. Open up a new file called main.js in your favourite editor. Now, just to make sure that everything is working at this early stage you can copy and paste the http server example from the nodejs.org front page and run that.

http.createServer(function (req, res) {
  setTimeout(function () {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello Worldn');
  }, 2000);
}).listen(8000);
$ node ./main.js

Navigate to http://127.0.0.1:8000/ and after 2 seconds you should see “Hello World” appear. If you see this, all is working. We have the (very basic) base of our application.

Talk to the Database

Once we know that our basic setup is working, we can try and interact with the database. Before we can do this, however, we actually need to create the database and add an record – or document.

CouchDB comes with a nifty little utility called Futon. We’ll use this to quickly create our database and add a document.

  • Go to http://localhost:5984/_utils/
  • Create a new database: erdnodeflips
  • Create a new document
  • Add Field – name it name
  • Double click on the value for name, enter “Don’t forget” (including the quotes)
  • Add another field – name it items
  • Double click on the value for items enter: ["item 1", "item 2", "item 3"] (including quotes and square brackets)
  • Click on Save Document – copy the _id value
  • Navigate to: http://localhost:5984/erdnodeflips/[value of "_id"] to see something like: {“_id”:”2d36f401bc4b82c9160e1a4ea936aba3″,”_rev”:”2-3b9f738a50ff9928348ce32b0eb807b2″,”items”:["item 1","item 2","item 3"],”name”:”Rob’s List”}

To read this within node we simply need to know the _id of the document. Save this as a var for now:

var doc_id = ’2d36f401bc4b82c9160e1a4ea936aba3′;

and replace the setTimeout function with the following:

db.getDoc(doc_id, function(error, doc) {
	if(error) {
	    res.write(JSON.stringify(error));
	}
	else {
	    res.write('Fetched my new doc from couch:');
	    res.write(JSON.stringify(doc));
	}
     res.end("-= Fin =-");
    });

This is pretty simple, and should be relatively straight forward to read. Essentially the code tries to get the document identified by doc_id, invoking a function when done: db.getDoc(doc_id, function(error, doc){});

The function that is invoked has 2 arguments, one containing the error (if any) and the second containing the actual document returned (if any).

Within this function we can then deal with the two arguments as needed. If there is an error, we output that to the browser, if no error and a doc is returned, output that instead.

Finally, we end the server response: res.end(“-= Fin =-”);. Note, we must end the server response within the callback of the getDoc() function. Due to the event driven, asynchronous nature of Node, if we end the response after that function then the server response will more than likely be closed before getDoc() has had a chance to write the additional server responses.

So the code now looks like this:

var sys = require('sys'),
    fs = require('fs'),
    http = require('http');
 
// Node-CouchDB: http://github.com/felixge/node-couchdb
var couchdb = require('./libs/node-couchdb/lib/couchdb'),
    client = couchdb.createClient(5984, 'localhost'),
    db = client.db('erdnodeflips');
 
// Haml-js: http://github.com/creationix/haml-js
var haml = require('./libs/haml-js/lib/haml');
 
var doc_id = '2d36f401bc4b82c9160e1a4ea936aba3';
 
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    db.getDoc(doc_id, function(error, doc) {
	if(error) {
	    res.write(JSON.stringify(error));
	}
	else {
	    res.write('Fetched my new doc from couch:');
	    res.write(JSON.stringify(doc));
	}
     res.end("-= Fin =-");
    });
 
}).listen(8000);
sys.puts('Server running at http://127.0.0.1:8000/');

Generate Nicer Output

If you run the above code, you should get something along the lines of:

Fetched my new doc from couch:{“_id”:”2d36f401bc4b82c9160e1a4ea936aba3″,”_rev”:”2-3b9f738a50ff9928348ce32b0eb807b2″,”items”:["item 1","item 2","item 3"],”name”:”Rob’s List”}-= Fin =-

So it works, but it doesn’t look very good.

First, lets create a Haml template that we can use to output something better.

!!! Strict
%html(lang="en")
  %head
    %title&= title
  %body
    %h1&= title
    %p&= message
    :if items.length === 0
      %p There are no items to remember
    :if items.length > 0
      %ul
        :each item in items
          %li&=

I’m not going to go into great depth. You can find more information on Haml at the Haml homepage and also have a look at Tim’s excellent tutorial on his Haml-js library.

Essentially, this template is going to build an XHTML strict page that will have a title, a message and then should loop through the items in the list. To get this displayed, we then need to open up the template file, render it and then output it as a server response. The code to do that is:

fs.readFile('./templates/doc.haml', function(e, c) {
		var data = {
		    title: "Erdnodeflip document: "+doc.name,
		    message: "Your Erdnusflip document was found!",
		    items: doc.items,
		};
		var html = haml.render(c.toString(), {locals: data});
		res.end(html);
	    });

Notice that I am simply ending the response with the generated HTML. Again, it is worth keeping an eye on exactly where the response is being ended otherwise you might find you are trying to add to a conversation that has already ended.

We can do the same for the “document not found” route:

!!! Strict
%html(lang="en")
  %head
    %title&= title
  %body
    %h1&= title
    %p&= message
    %p %a{href: link}&= link_text
fs.readFile('./templates/no-doc.haml', function(e, c) {
		var data = {
		    title: "No Document Found",
		    message: "No document could be found",
		    link: "/create",
		    link_text: "Create a new document"
		};
		var html = haml.render(c.toString(), {locals: data});
		res.end(html);

Subscribe to the mailing list

If you are enjoying this post, please consider signing up to the mailing list.


Quick Recap for this Tutorial

In this tutorial we’ve only scratched the surface of what can be done. However, in a relatively short space of time we have our Node web server communicating with a CouchDB database and displaying some valid strict XHTML. Not bad for one sitting!

To recap, our code now looks like this:

var sys = require('sys'),
    fs = require('fs'),
    http = require('http');
 
// Node-CouchDB: http://github.com/felixge/node-couchdb
var couchdb = require('./libs/node-couchdb/lib/couchdb'),
    client = couchdb.createClient(5984, 'localhost'),
    db = client.db('erdnodeflips');
 
// Haml-js: http://github.com/creationix/haml-js
var haml = require('./libs/haml-js/lib/haml');
 
var doc_id = '2d36f401bc4b82c9160e1a4ea936aba3';
 
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    db.getDoc(doc_id, function(error, doc) {
	if(error) {
	    fs.readFile('./templates/no-doc.haml', function(e, c) {
		var data = {
		    title: "No Document Found",
		    message: "No document could be found",
		    link: "/create",
		    link_text: "Create a new document"
		};
		var html = haml.render(c.toString(), {locals: data});
		res.end(html);
	    });
	}
	else {
	    fs.readFile('./templates/doc.haml', function(e, c) {
		var data = {
		    title: "Erdnodeflip document: "+doc.name,
		    message: "Your Erdnusflip document was found!",
		    items: doc.items,
		};
		var html = haml.render(c.toString(), {locals: data});
		res.end(html);
	    });
	}
    });
 
}).listen(8000);
sys.puts('Server running at http://127.0.0.1:8000/');

The exciting thing here is that we’ve thrown in a link! However, if you click on it, it will simply reload the page.

Being able to handle this kind of basic page navigation and actually adding stuff to the database is for next time.

Feedback always welcome. You can find all the code on my GitHub pages.

Valuable Node resources:

  • Pingback: » First Steps with Node.js: exciting stuff

  • Pingback: NodeJS Tutorial – Part 2, Routing

  • rasputnik

    Thanks, this was a good quickstart. Personally I’d probably skip node and just use Mustache directly in a couch view, though.

    Also, it seems like you’re loading the templates from disk for every http request, which I think will slow things down dramatically.

    • http://www.robsearles.com Rob Searles

      Hi Rasputnik,

      Hmm, going to have a look at Mustache – thanks for the pointer.

      Totally agree with the loading the templates for every request. Ideally they should be loaded in to memory . When I have time I’m going to finish off this tutorial, and I’ll be focusing on optimising it for “real world” use.

      Many thanks for your comment
      Rob

      • http://profiles.google.com/mordocai Chris Carpenter

         This is really late but…
        The OS will cache things like templates for you… unless the machine it is running on is low on memory and/or using a lot of disk IO intensive operiations, you shouldn’t have to manually implement your own memory management for things like this.

  • rasputnik

    Thanks, this was a good quickstart. Personally I'd probably skip node and just use Mustache directly in a couch view, though.

    Also, it seems like you're loading the templates from disk for every http request, which I think will slow things down dramatically.

  • http://www.robsearles.com Rob Searles

    Hi Rasputnik,

    Hmm, going to have a look at Mustache – thanks for the pointer.

    Totally agree with the loading the templates for every request. Ideally they should be loaded in to memory . When I have time I'm going to finish off this tutorial, and I'll be focusing on optimising it for “real world” use.

    Many thanks for your comment
    Rob

  • Pingback: Link dump for June 7th | The Queue Blog

  • http://twitter.com/tiefenb Markus Tiefenbacher

    i get an error, but don’t know whats wrong…

    SyntaxError: Unexpected token ;
    at Function. (/Users/markus/Sites/node/libs/haml-js/lib/haml.js:474:11)
    at execute (/Users/markus/Sites/node/libs/haml-js/lib/haml.js:480:8)
    at Function.render (/Users/markus/Sites/node/libs/haml-js/lib/haml.js:467:12)
    at /Users/markus/Sites/node/main.js:37:19
    at fs:84:13
    at node.js:773:9

    • Someone

      the reason is the usage of & and such things. replace them by the correct character and everything should work fine.

    • http://www.google.com/profiles/Matthias.A.Lee Matthias Lee

      I ran into the same issue, its the HTML-alized and(&) and greater-than(>) signs in your haml templates just take em out and itll work.
      so this:
      %title& amp;= title
      turns into:
      %title&= title

      and:
      :if items.length & gt; 0
      turns into:
      :if items.length > 0

      • http://www.google.com/profiles/Matthias.A.Lee Matthias Lee

        Also the last line of doc.haml should say:
        :each item in items
        %li&=item

  • http://twitter.com/TIEFENB Markus T.

    i get an error, but don't know whats wrong…

    SyntaxError: Unexpected token ;
    at Function.<anonymous> (/Users/markus/Sites/node/libs/haml-js/lib/haml.js:474:11)
    at execute (/Users/markus/Sites/node/libs/haml-js/lib/haml.js:480:8)
    at Function.render (/Users/markus/Sites/node/libs/haml-js/lib/haml.js:467:12)
    at /Users/markus/Sites/node/main.js:37:19
    at fs:84:13
    at node.js:773:9

  • Tj

    http://jade-lang.com is much better than haml-js, easier to write, better debugging, “proper” recursive descent parser

  • madmaze

    hmm i im running into a problem with couchdb.js

    /home/madmaze/projects/nodejs/flips/libs/node-couchdb/lib/couchdb.js:50
    continue;
    ^^^^^^^^

    node.js:116
    throw e; // process.nextTick error, or ‘error’ event on first tick
    ^

    • gooseus

      edit libs/node-couchdb/lib/couchdb.js ~ line 50:

      if (target === d.value) {
      //continue;
      return;
      }

  • http://www.google.com/profiles/Matthias.A.Lee Matthias Lee

    I ran into the same issue, its the HTML-alized and(&) and greater-than(>) signs in your haml templates just take em out and itll work.
    so this:
    %title& amp;= title
    turns into:
    %title&= title

    and:
    :if items.length & gt; 0
    turns into:
    :if items.length > 0

  • http://www.google.com/profiles/Matthias.A.Lee Matthias Lee

    Also I got rid of the !!! Strict
    and the last line of doc.haml should say:
    :each item in items
    %li&=item

  • gooseus

    edit libs/node-couchdb/lib/couchdb.js ~ line 50:

    if (target === d.value) {
    //continue;
    return;
    }

  • Pingback: [digital:meditation]» Blog Archive » a clueless start to node.js

  • http://profiles.google.com/mordocai Chris Carpenter

     This is really late but…
    The OS will cache things like templates for you… unless the machine it is running on is low on memory and/or using a lot of disk IO intensive operiations, you shouldn't have to manually implement your own memory management for things like this.