A Simple Blog with CouchDB, Bogart, and Node.js

2011-09-20 07:53

A Simple Blog with CouchDB, Bogart, and Node.js

by nrstott@gmail.com (Nathan Stott)

at 2011-09-19 23:53:58

original http://howtonode.org/bogart-couchdb

Update: By request I have posted a gist of the app.js using MongoDB instead of CouchDB. This gist also serves as a beginning example for how to use non-promise-based APIs with bogart.

In this article, you will learn how to use Bogart and CouchDB to create a minimal blogging engine. The Express with MongoDB article was a huge hit. This article has similar goals but shows a different way of using Node.JS.

Pre-Requisites

npm

npm is the most popular package manager for Node.js. Installing npm is easy.

curl http://npmjs.org/install.sh | sh

A note for windows users: npm does not currently work on windows. It will in the future.

Bogart

Bogart is a Sinatra-like framework designed to make it easy to create JSGI compliant web applications for node.js.

Bogart is in the npm registry.

npm install bogart

CouchDB

CouchDB is a document-oriented database with a RESTful interface. CouchDB works well with JavaScript since CouchDB speaks JSON. Also, CouchDB is queried using 'views' that are, by default, written in JavaScript. Download the latest release from here. CouchBase also maintains debian and rpm packages for the community.

JSGI

Bogart is a JSGI-based framework. JSGI is specified by the CommonJS mailing list. Knowledge of JSGI is helpful when dealing with Bogart; however, it is not necessary. You can find more information about JSGI on the CommonJS wiki.

Mustache

Mustache is a minimal templating engine with . Mustache is the default templating engine of Bogart. When you install Bogart, you will also be installing Mustache.

CouchDB-CommonJS

CouchDB-CommonJS is a promise-based CouchDB library available in the npm registry. It can also be used in the browser or with Narhwal.

npm install couchdb

What will our application do?

To keep things simple, we're going to only tackle basic functionality. Our blog application will support the following methods:

  • Create a new post (POST /posts)
  • Show a list of all the posts (GET /posts)
  • Show a single post (GET /posts/:id)
  • Comment on a post (POST /posts/:id/comments)

Lets get started!

A Bogart application consists of a JSGI server with one or more pieces of middleware and one or more Bogart routers each containing any number of routes.

The canonical 'Hello World' application in Bogart can be written as follows:

hello-world.js
var bogart = require('bogart');

var router = bogart.router();

router.get('/', function() {
  return bogart.html('Hello World');
});

bogart.start(app);

This JavaScript program defines a single route that accepts GET requests to the root of the site and returns a simple HTML greeting.

To run this program, first execute the following commands to setup your blog directory:

mkdir bogart-couchdb-blog
cd bogart-couchdb-blog
npm install bogart

This will create a new directory named bogart-couchdb-blog and install bogart to the node_modules subdirectory of this directory. Next, copy the JavaScript into a file into bogart-couchdb-blog and name it hello-world.js and then execute

node hello-world.js

Visit http://localhost:8080 in your browser.

Creating the package.json file

In order to manage dependencies, it is useful to create a package.json file. This file provides details on the packages you depend on so that you can more easily use npm to manage these dependencies.

Create a file named package.json in your bogart-couchdb-blog directory.

package.json
{
    "name": "blog",
    "description": "Simple Blogging Engine",
    "version": "0.1.0",
    "author": "Nathan Stott",
    "email": "nathan.stott@whiteboard-it.com",
    "main": "./app",
    "directories": { "lib": "./lib" },
    "dependencies": {
      "bogart": ">=0.2.0",
      "mustache": "0.3.1-dev",
      "couchdb": ">=0.1.2",
      "promised-io": "=0.2.3"
    }
}

The most important field in this JSON file is the dependencies field. This field will allow you to execute npm install to install the dependencies for your project.

Creating a Post

There are two routes that we will need in order to create a post.

  • GET /posts/new -> returns a form to create a new post
  • POST /posts -> creates a new post from the form parameters provided

The mustache template to create a new post is as follows:

new-post.html
<form method='post' action='/posts'>
  <fieldset>
    <legend>New Post</legend>

    <div>
      <label for='title'>Title</label>
      <input name='title' />
    </div>

    <div>
      <label for='body'>Body</label>
      <textarea name='body' rows='15' columns='25'></textarea>
    </div>

    <div class='buttons'>
      <input type='submit' value='Save Post' />
    </div>
  </fieldset>
</form>

This post will be rendered inside of a layout to keep the look of the site consistant. By convention, Bogart's view engine uses a file called layout.html as the layout if it exists. A Bogart layout is a template with a } tag to include the view inside of the layout.

layout.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html>
<head>
  <title></title>
</head>
<body>
  }
</body>
</html>

The route to return the new post template makes use of the bogart.respond helper. Even though it is not strictly necesarry to understand JSGI in order to use Bogart, lets go over the basic concept of a JSGI response. Bogart routes expect a JSGI response or a promise that will resolve to a JSGI response to be returned. A JSGI response is an object that contains three attributes: status (required), body (required), and headers (optional).

A simple JSGI response:

{
  status: 200,
  body: [ 'Hello World' ]
}

The Bogart route to render new-post.html is as follows:

router.get('/posts/new', function(req) {
  return viewEngine.respond('new-post.html', {
    locals: {
      title: 'New Post'
    }
  })
});

viewEngine should be defined at the beginning of the Bogart configuartion closure as

viewEngine = bogart.viewEngine('mustache');

Bogart supports mustache out of the box. There is a jade view engine in the package bogart-jade. If you want to use jade then npm install bogart-jade and require('bogart-jade'). After that, bogart.viewEngine('jade') will work. It is easy to add support for more view engines as well.

Bogart includes useful middleware to make working with forms easy. Normally, req.body will contain the raw body of a form post. It is more conveniant if this is automatically converted to a JSON object for us. The Bogart middleware Parted accomplishes this.

Adding middleware is easiest using a Bogart Application object. JSGI Parted middleware wraps the streaming multipart, json, and urlencoded parsing utility Parted.

var app = bogart.app();
app.use(bogart.middleware.Parted);
app.use(router);
app.start();

Lets take a side-step to discuss how we can work with CouchDB using the couchdb package from the npm registry.

At the top of our app.js add the following:

var couchdb = require('couchdb');

In the closure that configures Bogart routes, create a couchdb client and a database representation.

var app = bogart.router(function(get, post, put, destroy) {

  var client     = couchdb.createClient(5984, '127.0.0.1', { user: 'myuser', password: 'mypass' })
    , db         = client.db('blog')
    , viewEngine = bogart.viewEngine('mustache');

  // configure routes...
});

This creates a couchdb client connecting to '127.0.0.1' and port 5984. If your CouchDB is in Admin Party, you do not need to supply the user and password in an options hash. If you have a CouchDB users setup, please provide your username and password.

Now we will create a route to handle the POST of our form.

router.post('/posts', function(req) {
  var post = req.params;
  post.type = 'post';

  return db.saveDoc.then(function(resp) {
    return bogart.redirect('/posts');
  });
});

We add the type attribute to the post so that as we add more document types in the future, we can easily create CouchDB views to find only specific document types. This is not a built-in CouchDB concept. It is a useful convention that makes creating views simpler.

Adding a CouchDB view to retrieve posts

CouchDB is queried using map/reduce views that are defined on design documents. This means that we need to create a design document before we can query a list of the posts in our database.

Lets create a JavaScript file syncDesignDoc.js in the lib directory of our project.

syncDesignDoc.js
var couchdb = require('couchdb')
  , settings = require('../settings');

var client  = couchdb.createClient(settings.port, settings.host, { user: settings.user, password: settings.password });
var db      = client.db(settings.db);

var designDoc = {
  _id: '_design/blog',

  language: 'javascript',

  views: {
    'posts_by_date': {
      map: function(doc) {
        if (doc.type === 'post') {
          emit(doc.postedAt, doc);
        }
      }.toString()
    }
  }
};

db.saveDoc(designDoc).then(function(resp) {
  console.log('updated design doc!');
}, function(err) {
  console.log('error updating design doc: '+require('util').inspect(err));
});

Execute node lib/syncDesignDoc.js to update the database with the latest design document.

Listing Posts

Lets create a Mustache template to list the posts from our database.

posts.html
<h2>Posts</h2>

<a href='/posts/new'>New Post</a>

<ul>
  
    <li>
      <div class='post'>
        <h2><a href='/posts/'></a></h2>
        <p>
          
        </p>
      </div>
    </li>
  
</ul>

Next, lets create a Bogart route to render this template. We will query the database using db.view, process the response from CouchDB, and respond with the rendered template. Bogart makes this easy:

  router.get('/posts', function(req) {

    return db.view('blog', 'posts_by_date').then(function(resp) {
      var posts = resp.rows.map(function(x) { return x.value; });

      return viewEngine.respond('posts.html', {
        locals: {
          posts: posts
        }
      });
    });
  });

Show an Individaul Post

It's time to create a route to show an individual post. This page will also contain a form for adding comments.

The template will be as follows:

post.html
<a href='/posts'>Home</a>

<h1></h1>

<div class='post-content'>
  
</div>

<div>
  <h2>Comments</h2>
  <ul id='comments'>
    
      <li>
        <div>
          Author: <span class='author'></span>
        </div>
        <div>
          
        </div>
      </li>
    

    
      No Comments Yet
    
  </ul>
</div>

<form method='post' action='/posts//comments'>
  <fieldset>
    <legend>Leave a Comment</legend>

    <div>
      <label for='author'>Your Name</label>
      <input name='author' />
    </div>

    <div>
      <label for='body'>Your Comment</label>
      <input name='body' />
    </div>

    <div class='buttons'>
      <input type='submit' value='Post Comment' />
    </div>
  </fieldset>
</form>

The Bogart route to display this is as simple as the route to display the form for creating new posts.

router.get('/posts/:id', function(req) {
  return db.openDoc(req.params.id).then(function(post) {
    return viewEngine.respond('post.html', { locals: post });
  });
});

The route to accept the POST from the comments form is similar to the route to accept a new blog post:

router.post('/posts/:id/comments', function(req) {
  var comment = req.params;

  return db.openDoc(req.params.id).then(function(post) {
    post.comments = post.comments || [];
    post.comments.push(comment);

    return db.saveDoc(post).then(function(resp) {
      return bogart.redirect('/posts/'+req.params.id);
    });
  });
});

Summing it up

As you can see, getting started with Bogart and CouchDB is simple. We are a long way from having a full-featured blog, but hopefully this will inspire some out there to try working with Node.JS, Bogart, and CouchDB!

The full source code of the finished app.js is below:

app.js
var bogart  = require('bogart')
  , couchdb = require('couchdb')
  , settings = require('./settings');

var client     = couchdb.createClient(settings.port, settings.host, { user: settings.user, password: settings.password })
  , db         = client.db(settings.db)
  , viewEngine = bogart.viewEngine('mustache')
  , router     = bogart.router();

router.get('/', function(req) {
  return bogart.redirect('/posts');
});

router.get('/posts', function(req) {

  return db.view('blog', 'posts_by_date').then(function(resp) {
    var posts = resp.rows.map(function(x) { return x.value; });

    return viewEngine.respond('posts.html', {
      locals: {
        posts: posts,
        title: 'Blog Home'
      }
    });
  }, function(err) {
    if (err.error && err.error === 'not_found') {
      return bogart.error('execute syncDesignDoc before trying to use the blog');
    }
    throw err;
  });
});

router.get('/posts/new', function(req) {
  return viewEngine.respond('new-post.html', {
    locals: {
      title: 'New Post'
    }
  });
});

router.post('/posts', function(req) {
  var post = req.params;
  post.type = 'post';
  post.postedAt = new Date();

  return db.saveDoc(post).then(function(resp) {
    return bogart.redirect('/posts');
  });
});

router.get('/posts/:id', function(req) {
  return db.openDoc(req.params.id).then(function(post) {
    return viewEngine.respond('post.html', { locals: post });
  });
});

router.post('/posts/:id/comments', function(req) {
  var comment = req.params;

  return db.openDoc(req.params.id).then(function(post) {
    post.comments = post.comments || [];
    post.comments.push(comment);

    return db.saveDoc(post).then(function(resp) {
      return bogart.redirect('/posts/'+req.params.id);
    });
  });
});

var app = bogart.app();

// Include batteries, a default JSGI stack.
app.use(bogart.batteries);

// Include our router, it is significant that this is included after batteries.
app.use(router);

app.start();