Introduction

This guide provides information for developers who want to use the Couchbase Node.js SDK to build applications that use Couchbase Server.

Getting Started

The following sections demonstrate how to get started using Couchbase with the Node.js SDK. We’ll first show how to install the SDK and then demonstrate how it can be used to perform some simple operations.

Download and Installation

The Node.js SDK is compatible only with Node.js v0.8, v0.10 and v0.11. Because the SDK has a C++ component, you also need a C++ compiler and associated build tools. The SDK package for Microsoft Windows ships with prebuilt binaries.

To get started with the Node.js SDK, install the following packages:

  1. Download, install and start Couchbase server.

  2. Install the Node.js SDK. The easiest way to do this is via the npm tool:

    shell> npm install couchbase
    

    If all goes well, you won’t see any errors printed to the screen.

Hello Couchbase

To follow the tradition of programming tutorials, we’ll start with “Hello Couchbase”. This example works with the “beer-sample” bucket that is provided with the default install.

hello-couchbase.js

var couchbase = require("couchbase");

var bucket = new couchbase.Connection({
  'bucket':'beer-sample',
  'host':'127.0.0.1:8091'
}, function(err) {
  if (err) {
    // Failed to make a connection to the Couchbase cluster.
    throw err;
  }

  bucket.get('aass_brewery-juleol', function(err, result) {
    if (err) {
      // Failed to retrieve key
      throw err;
    }

    var doc = result.value;

    console.log(doc.name + ', ABV: ' + doc.abv);

    doc.comment = "Random beer from Norway";

    bucket.replace('aass_brewery-juleol', doc, function(err, result) {
      if (err) {
        // Failed to replace key
        throw err;
      }

      console.log(result);

      // Success!
      process.exit(0);
    });
  });
});

The following points explain each step in the example:

  • Connecting

    A Connection object represents a connection to a single bucket within the cluster.

    A bucket represents a logical namespace for a key. All keys must be unique within a single bucket, but multiple buckets can have keys with the same names and they will not conflict. A new connection object must be created for each bucket that you want to interact with in your application. This example creates one connection to the beer-sample bucket.

    The constructor is passed the bucket name, which is beer-sample, and a node on the cluster to connect to. You can pass any node that is a member of the cluster. This example uses the local cluster instance.

  • Retrieving Data

    The get method initiates an asynchronous request to retrieve the key requested. If the key exists, the callback will be invoked with a results object that contains the value of the key as well as any additional metadata returned from the cluster. To get the actual value of the object, you can access the result object’s value property.

    If the key does not exist on the server, the callback will be invoked with an unset value property on the result object (it will not set the callbacks error parameter).

  • Storing Data

    To store documents in the server, you can use one of the set family of methods. Here we use replace, which enforces the constraint that a previous value of the document must already exist. This can be thought of as an update operation in terms of CRUD (create, read, update, delete).

    The storage methods also return a result object that contains metadata about the value that was stored.

Now we’re ready to run our first Couchbase Program:

shell> node hello-couchbase.js

Working with Documents

A document in Couchbase server consists of a key, value, and metadata.

  • Key

    A key is a unique identifier for your data. Each document must have a unique key. The key can be any valid string.

  • Value

    The value is your own application data that exists under the key. The format of the value can be anything, couchnode will automatically choose the best storage format.

  • Metadata

    The metadata contains information concerning the format of the value that is, whether it’s JSON, a raw type, or something else. It also contains revision information such as the CAS, which we’ll read about later.

You can store documents by providing the unique key under which the document will be stored, and the value which contains the actual document. You can retrieve documents either by directly specifying the unique key under which the document was stored or by querying views that retrieve documents based on specific criteria.

Getting a document by key

Couchbase provides two ways to fetch your documents: you can retrieve a document by its key, or you can retrieve a set of documents that match some constraint by using Views. Because views are more complex, we’ll first demonstrate getting documents by their keys.

Getting A Single Document

To get a single document, simply supply the key as the first argument to the get method. It returns a result object on success that can then be used to extract the value.

bucket.get(‘test1’, function(err, result) {
  console.log(result.value);
});

Getting Multiple Documents

To get multiple documents, you can use the more efficient getMulti method. You can pass it an array of keys and it will return an object containing keys that match the keys you passed into getMulti and result objects similar to the get call as values.

bucket.getMulti([‘test1’, ‘test2’], function(err, results) {
  for(key in results) {
    console.log(key + ‘: ‘ + results[key].value);
  }
});

Error Handling

If the key does not exist on the server, the callback will be invoked with an unset value property on the result objects (it will not set the callbacks error parameter). In the case of a getMulti call, it is possible that some results will be returned, while others will not. For every other kinds of error such as temporaryError, it is possible that some result objects will have error set, while others do not. If any of the results from a getMulti call failed, there error parameter of the callback will be set to 1 to signify that at least one operation failed to execute successfully.

Storing a Document

This section provides a bit more insight on how to store documents. This is a prerequisite to demonstrate how to retrieve documents because there must be something to retrieve.

There are additional storage methods beyond those described here, which are covered in the Advanced section, see Chapter 4, Advanced Usage. These include manipulating numeric counters, setting expiration times for documents, and appending/prepending to existing values.

The Connection object provides the following store operations, which conform to the CRUD model:

  • set(key, value, options)

    Stores the document value under the key. If the key did not previously exist, it is created. If the key already exists, its existing value is overwritten with the new contents of value.

  • add(key, value, options)

    Stores the document value under the key, but only if key does not already exist. If key already exists, an exception is thrown.

  • replace(key, value, options)

    Replace is the inverse of add. It sets the contents of key to value, but only if the key already exists. If the key does not already exist, an exception is thrown.

  • remove(key, options)

    Deletes the key from the bucket. Future attempts to access this key via get raise an exception until something is stored again for this key using one of the set methods.

The following code demonstrates the store operations.

var couchbase = require('couchbase');

var key = "demo_key";
var value = "demo_value";

// We use the 'default' bucket.
bucket = new couchbase.Connection({bucket: 'default', host:'localhost'}, function(err) {
  if (err) throw err;

  console.log('Setting key ' + key + ' with value ' + value);
  bucket.set(key, value, function(err, result) {
    if (err) throw err;
    console.log(result);

    console.log('Getting value for key ' + key);
    bucket.get(key, function(err, result) {
      if (err) throw err;
      console.log(result);

      console.log('Creating new key ' + key + ' with value "new_value"');
      console.log('This will fail as ' + key + ' already exists');
      bucket.add(key, 'new_value', function(err, result) {
        console.log(err);

        console.log('Replacing existing key ' + key + ' with new value');
        bucket.replace(key, 'new value', function(err, result) {
          if (err) throw err;
          console.log(result);

          console.log('Getting new value for key ' + key);
          bucket.get(key, function(err, result) {
            if (err) throw err;
            console.log(result);

            console.log('Deleting key ' + key);
            bucket.remove(key, function(err, result) {
              if (err) throw err;
              console.log(result);

              console.log('Getting value for key ' + key);
              console.log('This will fail as it has been deleted');
              bucket.get(key, function(err, result) {
                console.log(err);

                // Done
                process.exit(0);
              });
            });
          });
        });
      });
    });
  });
});

Output:

Setting key demo_key with value demo_value { cas: { '0': 467599872, '1': 461666438 } } Getting value for key demo_key { cas: { '0': 467599872, '1': 461666438 }, flags: 4, value: 'demo_value' } Creating new key demo_key with value "new_value" This will fail as demo_key already exists { [Error: Key exists (with a different CAS value)] code: 12 } Replacing existing key demo_key with new value { cas: { '0': 467599872, '1': 3664555910 } } Getting new value for key demo_key { cas: { '0': 467599872, '1': 3664555910 }, flags: 4, value: 'new value' } Deleting key demo_key { cas: { '0': 467599872, '1': 3681333126 } } Getting value for key demo_key This will fail as it has been deleted { [Error: No such key] code: 13 }

Querying a view

In addition to fetching documents by keys, you can also employ Views to retrieve information using secondary indexes. This guide gets you started on how to use them from the Node.js SDK. If you want to learn more about views, see the chapter in the Couchbase Server 2.0 documentation.

First, create your view definition using by the web UI (though you may also do this directly from the Node.js SDK, as will be shown later).

You can then query the view results by calling the query method on the Connection object. Simply pass it the design and view name.

The view method returns a ViewQuery object which provides an interface for accessing the data stored in a view. The view can be then queried via the ViewQuery method through the use of the query or firstPage methods which return a full list of results, or only the first page respectively. Both the view method as well as the query methods accept additional optional options that control the behavior of the results returned. You can thus use it as follows:

var viewQuery = bucket.view('beer', 'brewery_beers', {opt1: value1, opt2: value2});
viewQuery.query(function(err, results) {
  for(key in results) {
    // Do something with the results
  }
});

Or alternatively like this to retrieve only the first 30 results:

var viewQuery = bucket.view('beer', 'brewery_beers', {opt1: value1, opt2: value2});
viewQuery.firstPage({limit:30}, function(err, results) {
  for(key in results) {
    // Do something with the results
  }
});

Here are some of the available options for the query methods. A full listing can be found in the API documentation.

  • reduce

    This boolean parameter indicates whether the server should also pass the results to the view’s reduce function. An error is triggered if the view does not have a reduce method defined.

  • limit

    This numeric parameter indicates the maximum amount of results to fetch from the query. This parameter is handy if your query can produce a lot of results.

  • descending

    This boolean parameter indicates that the results should be returned in reverse order.

  • stale

    This string parameter controls the trade-off between performance and freshness of data. Possible values include “false” (force update), “update_after” (force after view request) and “ok” (default: do not force any updates).

  • debug

    This boolean parameter fetches low-level debugging information from the view engine.

Tutorial

In this chapter we build on the foundations of the Getting Started guide and build a simple web application. Make sure you have the beer-sample bucket installed, because we’ll be using it. The sample application will allow you to edit and manage various beers and breweries.

This tutorial is not entirely complete, and leaves some features that remain to be implemented. Implementing them is left as an exercise for the reader.

The full source code for the sample application is available in the beersample-node project through couchbaselabs on GitHub.

Note that the sample application provides more content than described in this tutorial, but it should be simple to navigate while reading this tutorial.

Quickstart

  • Download Couchbase Server and install it. Make sure to install the beer-sample dataset when you run the wizard because this tutorial application works with it.

  • Clone the repository and install dependencies (express, jade):

    shell> git clone git://github.com/couchbaselabs/beersample-node Cloning into 'beersample-node' #... shell> cd beersample-node shell> npm install

  • Some views need to be set up. You can set up the views manually via the Web UI, or invoke the beer.js script located in the beersample-node directory with the –setup parameter.

    In the beer design document, create a development view called by_name, then promote it to a production view (more details later on why this is neccessary):

    function (doc, meta) {
         if (doc.type && doc.type == "beer") {
             emit(doc.name, null);
         }
     }
    

    Create another design document called brewery and add a view called by_name (dont forget to promote it to a production view as above!):

    function (doc, meta) {
          if (doc.type && doc.type == "brewery") {
              emit(doc.name, null);
          }
      }
    
  • Invoke the beer.js script:

    shell> node beer.js Server running at http://127.0.0.1:1337/

  • Navigate to http://localhost:1337/welcome and enjoy the application!

Preparations

Project Setup

In this section we’ll talk a bit about setting up your directory layout and adding some views in the server before we start dealing with the Node.js SDK and express+jade itself.

Create a project directory named beer:

shell> mkdir beer shell> cd beer shell> mkdir views shell> mkdir views/beer shell> mkdir views/brewery shell> mkdir static shell> mkdir static/js shell> mkdir static/css Showing your directory contents displays something like this:

shell> find. -type d./static./static/js./static/css./views./views/brewery./views/beer To make the application look pretty, we’re incorporating jQuery and Twitter Bootstrap. You can either download the libraries and put them in their appropriate css and js directories (under static), or clone the project repository and use it from there. If you followed the Quickstart steps, you already have the files in your beersample-node directory. Either way, make sure you have the following files in place:

static/css/beersample.css static/css/bootstrap.min.css (the minified twitter bootstrap library) static/css/bootstrap-responsive.min.css (the minified responsive layout classes from bootstrap) static/js/beersample.js static/js/jquery.min.js (the jQuery javascript library) From here on, you should have a bare bones web application configured that has all the dependencies included. We’ll now move on and configure the beer-sample bucket the way we need it.

Preparing the Views

The beer-sample bucket comes with a small set of predefined views, but to make our application function correctly we need some more. This is also a good chance to explore the view management possibilities inside the Web-UI.

Because we want to list beers and breweries by name, we need to define one view for each. Head over to the Web-UI and click on the Views menu. Select beer-sample from the drop-down list to switch to the correct bucket. Now click on Development Views and then Create Development View to define your first view. You need to give it the name of both the design document and the actual view. Insert the following names:

  • Design Document Name: _design/dev_beer

  • View Name: by_name

The next step is to define the map and (optional) reduce functions. In our examples, we won’t use the reduce functions at all but you can play around and see what happens. Insert the following map function (that’s JavaScript) and click Save.

function (doc, meta) {
  if(doc.type && doc.type == "beer") {
    emit(doc.name, null);
  }
}

Every map function takes the full document (doc) and its associated metadata (meta) as the arguments. You are then free to inspect this data and emit a result when you want to have it in your index. In our case, we emit the name of the beer (doc.name) when the document both has a type field and the type is beer. We don’t need to emit a value — that’s why we are using null here. It’s always advisable to keep the index as small as possible. Resist the urge to include the full document through emit(meta.id, doc), because it will increase the size of your view indexes. If you need to access the full document you can call db.get(result.key) to get the individual doc for a single row. The resulting retrieval of the document might be slightly out of sync with your view, but it will be fast and efficient.

Now we need to define a view for our breweries. You already know how to do this — here is all the information you need to create a brewery view:

  • Design Document Name: _design/dev_brewery

  • View Name: by_name

  • Map Function:

    function (doc, meta) {
       if(doc.type && doc.type == "brewery") {
         emit(doc.name, null);
       }
     }
    

The final step is to push the design documents in production. While the design documents are in development, the index is applied only to a deterministic subset of the data. Because we want to have the index on the whole dataset, click the Publish button on both design documents (and accept any pop-up windows that warn you about overriding the old design documents).

For more information about using views for indexing and querying from Couchbase Server, see the following helpful resources in the Couchbase Server Manual:

The Welcome Page

The first route we will implement is that of the welcome page, that is, the page that is displayed when someone goes to the root of your site. Because there is no Couchbase interaction involved, we just tell express to render the template.

beer_app.js (welcome page):

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

The welcome.html template is actually a Jade template inside the views directory. It looks like this:

views/welcome.jade:

extends layout
block content
  .span6
    .span12
      h4 Browse all Beers
      a(href="/beers" class="btn btn-warning") Show me all beers
      hr
    .span12
      h4 Browse all Breweries
      a(href="/breweries" class="btn btn-info") Take me to the breweries
  .span6
    .span12
      h4 About this App
      p Welcome to Couchbase!
      p
        | This application helps you to get started on application
        | development with Couchbase. It shows how to create, update and
        | delete documents and how to work with JSON documents.

The template simply provides some links to the brewery and beer pages (which are shown later).

An interesting thing about this template is that it “inherits” from the common layout.jade template. All pages in the beer app have a common header and footer to them — with only their body differing. This is the layout.jade template.

views/layout.jade:

!!!5
html(lang="en")
  head
    meta(charset="utf-8")
    title Couchbase Node.js Beer Sample
    meta(name="viewport" content="width=device-width, initial-scale=1.0")
    meta(name="description" content="The Couchbase Node.js Beer-Sample App")
    meta(name="author" content="Couchbase, Inc. 2013")

    link(href="/css/bootstrap.min.css" rel="stylesheet")
    link(href="/css/beersample.css" rel="stylesheet")
    link(href="/css/bootstrap-responsive.min.css" rel="stylesheet")

    //HTML5 shim, for IE6-8 support of HTML5 elements
    //if lt IE 9
      script(src="http://html5shim.googlecode.com/svn/trunk/html5.js")

  body
    .container-narrow
      .masthead
        ul.nav.nav-pills.pull-right
          li: a(href="/welcome") Home
          li: a(href="/beers") Beers
          li: a(href="/breweries") Breweries
        h2.muted Couchbase Beer Sample
      hr

      .row-fluid
        .span12
          block content
      hr

      .footer
        p © Couchbase, Inc. 2013

    script(src="/js/jquery.min.js")
    script(src="/js/beersample.js")

The template simply provides some links to the brewery and beer pages (which are shown later).

If you start your app now, you should be able to navigate to http://localhost:1337/welcome and see the welcome page. You’ll get a 404 error if you try to visit any links though - this is because we haven’t implemented them yet. Let’s do that now!

Managing Beers

Showing Beers

Now we’re finally getting into the cooler stuff of this tutorial. In the beer listing page, we want to display each beer along with a link to the brewery that produces it. However, we’ve defined the beer/by_name view to return only the name of the beer. To obtain the brewery, we need to fetch each beer document and examine it. The document contains the brewery that we need later.

After we’ve retrieved a list of all the beers, we create a list of document IDs to fetch by using the underscore libraries pluck function. We pass this list to getMulti.

While we could have made this simpler by performing an individual get on each beer’s id, that would be less efficient in terms of network usage.

Now that we have the beer documents, we iterate through the list of values we retrieved and assign the key (which is the object key) to a property of the beer object itself to allow usage of it in the template.

Now let’s put this all together:

beer_app.js (showing beer listings):

function list_beers(req, res) {
      var q = {
        limit : ENTRIES_PER_PAGE,   // configure max number of entries.
        stale : false               // We don't want stale views here.
      };

      db.view( "beer", "by_name", q).query(function(err, values) {
        // 'by_name' view's map function emits beer-name as key and value as
        // null. So values will be a list of
        //      [ {id: <beer-id>, key: <beer-name>, value: <null>}, ... ]

        // we will fetch all the beer documents based on its id.
        var keys = _.pluck(values, 'id');

        db.getMulti( keys, null, function(err, results) {

          // Add the id to the document before sending to template
          var beers = _.map(results, function(v, k) {
            v.value.id = k;
            return v.value;
          });

          res.render('beer/index', {'beers':beers});
        })
      });
    }
    app.get('/beers', list_beers);

We also tell express to route requests for /beers to this function, we then direct express to render the beer/index.jade template.

Here is the beer/index.jade template:

views/index.jade:

extends ../layout
  block content

    h3 Browse Beers
    form(class="navbar-search pull-left")
      input#beer-search(class="search-query" type="text" placeholder="Search for Beers")

    table#beer-table(class="table table-striped")
      thead
        tr
          th Name
          th Brewery
          th
      tbody
        for beer in beers
          tr
            td: a(href="/beers/show/#{beer.id}") #{beer.name}
            td: a(href="/breweries/show/#{beer.brewery_id}") To Brewery
            td
              a(class="btn btn-small btn-warning" href="/beers/edit/#{beer.id}") Edit
              a(class="btn btn-small btn-danger" href="/beers/delete/#{beer.id}") Delete

    div
      a(class="btn btn-small btn-success" href="/beers/create") Add Beer

Navigate to http://localhost:1337/beers, to see a listing of beers. Each beer has To Brewery, Edit, and Delete buttons.

On the bottom of the page, you can also see an Add Beer button, which allows you to define new beers.

Let’s implement the Delete button next!

Deleting Beers

Due to the simplicity of Couchbase and express, we can implement a single method to delete both beers and breweries.

beer_app.js (showing beer listings):

function delete_object( req, res ) {
    db.remove( req.params.object_id, function(err, meta) {
      if( err ) {
        console.log( 'Unable to delete document `' + req.params.object_id + '`' );
      }

      res.redirect('/welcome');
    });
  }
  app.get('/beers/delete/:object_id', delete_object);
  app.get('/breweries/delete/:object_id', delete_object);

Here we tell express to route our two deletion URLs to the same method. We attempt to delete the object in our Couchbase cluster that is passed through the url then redirect the user to our Welcome page. If this delete fails, we also log an error to the console.

If you find that a beer is still displayed after you click the delete button, you can wait a moment and refresh the browser page to verify that the beer has been deleted. Deleted objects may not immediately get removed from our view, but may instead need to wait for a view index update.

Another way to verify that a beer has been deleted is by clicking the delete button again and getting a 404 error.

Displaying Beers

beer_app.js (showing a single beer):

function show_beer(req, res) {
    db.get( req.params.beer_id, function(err, result) {
      var doc = result.value;
      if( doc === undefined ) {
        res.send(404);
      } else {
        doc.id = req.params.beer_id;

        var view = {
          'beer': doc,
          'beerfields': _.map(doc, function(v,k){return {'key':k,'value':v};})
        };
        res.render('beer/show', view);
      }
    });
  }
  app.get('/beers/show/:beer_id', show_beer);

Like for the delete example, we first check if the document actually exists within our cluster. We pass the beer ID through the URL, this is passed to use as beer_id, as seen in the express route.

In order to retrieve the information or this particular beer, we simply call the connections get method with the beer_id we received through the route. We first check the ensure we received a document and if not return a HTTP 404 error.

If the beer exists, we build prepare a view object to pass to the template which contains the beer object as well as a mapped list of all fields and values that are inside of the beer object. We pass this data to the views/beer/show.jade template, which you is shown below.

views/beer/show.jade:

extends ../layout
block content

  h3 Show Details for Beer #{beer.name}
  table(class="table table-striped")
    tbody
      tr
        td: strong #{beer.brewery_id}
        td: a(href="/breweries/show/#{beer.brewery_id}") #{beer.brewery_id}
      for beerfield in beerfields
        tr
          td: strong #{beerfield.key}
          td #{beerfield.value}

  a(class="btn btn-medium btn-warning" href="/beers/edit/#{beer.id}") Edit
  a(class="btn btn-medium btn-danger" href="/beers/delete/#{beer.id}") Delete

Here we extract the brewery_id, and create a special entry with a link pointing to the page to display the actual brewery.

Then we is iterate over the rest of the fields, printing out the key and value of each.

Finally, we provide links at the bottom to Edit and Delete the beer.

Editing Beers

beer_app.js (beer editing):

function normalize_beer_fields(data) {
    var doc = {};
    _.each(data, function(value, key) {
      if(key.substr(0,4) == 'beer') {
        doc[key.substr(5)] = value;
      }
    });

    if (!doc['name']) {
      throw new Error('Must have name');
    }
    if (!doc['brewery_id']) {
      throw new Error('Must have brewery ID');
    }

    return doc;
  }

  function begin_edit_beer(req, res) {
    db.get(req.params.beer_id, function(err, result) {
      var doc = result.value;
      if( doc === undefined ) { // Trying to edit non-existing doc ?
        res.send(404);
      } else { // render form.
        doc.id = req.params.beer_id;
        var view = { is_create: false, beer: doc };
        res.render('beer/edit', view);
      }
    });
  }
  function done_edit_beer(req, res) {
    var doc = normalize_beer_fields(req.body);

    db.get( rc.doc.brewery_id, function(err, result) {
      if (result.value === undefined) { // Trying to edit non-existing doc ?
        res.send(404);
      } else {    // Set and redirect.
        db.set( req.params.beer_id, doc, function(err, doc, meta) {
          res.redirect('/beers/show/'+req.params.beer_id);
        })
      }
    });
  }
  app.get('/beers/edit/:beer_id', begin_edit_beer);
  app.post('/beers/edit/:beer_id', done_edit_beer);

We define two handlers for editing. The first handler is the GET method for /beers/edit/:beer_id, which displays a nice HTML form that we can use to edit the beer. It passes the following parameters to the template: the beer object and a boolean that indicates this is not a new beer (because the same template is also used for the Create Beer form).

The second handler is the POST method, which validates the input. The post handler calls the normalize_beer_fields function, which converts the form fields into properly formed names for the beer document, checks to see that the beer has a valid name, and checks to see that a brewery_id is specified and that it indeed exists. If all the checks pass, the function returns the formatted document. If an exception is thrown, express will catch this error and render it to the user. Otherwise, the document is set to Couchbase using the set method and the user is redirected to the newly created beer’s show page.

This template is rather wordy because we enumerate all the possible fields with a nice description.

views/beer/edit.jade:

extends ../layout
block content

  if is_create
    h3 Create Beer
  else
    h3 Editing #{beer.name}

  form(method="post" action="")
    fieldset
      legend General Info
      .span12
        .span6
          label Type
          input(type="text" name="beer_type" placeholder="Type of the document" value="#{beer.type}")
          label Name
          input(type="text" name="beer_name" placeholder="The name of the beer" value="#{beer.name}")
          label Description
          input(type="text" name="beer_description" placeholder="A short description" value="#{beer.description}")
        .span6
          label Style
          input(type="text" name="beer_style" placeholder="Bitter? Sweet? Hoppy?" value="#{beer.style}")
          label Category
          input(type="text" name="beer_category" placeholder="Ale? Stout? Lager?" value="#{beer.category}")
    fieldset
      legend Details
      .span12
        .span6
          label Alcohol (ABV)
          input(type="text" name="beer_abv" placeholder="The beer's ABV" value="#{beer.abv}")
          label Biterness (IBU)
          input(type="text" name="beer_ibu" placeholder="The beer's IBU" value="#{beer.ibu}")
        .span6
          label Beer Color (SRM)
          input(type="text" name="beer_srm" placeholder="The beer's SRM" value="#{beer.srm}")
          label Universal Product Code (UPC)
          input(type="text" name="beer_upc" placeholder="The beer's UPC" value="#{beer.upc}")
    fieldset
      legend Brewery
      .span12
        .span6
          label Brewery
            input(type="text" name="beer_brewery_id" placeholder="The brewery" value="#{beer.brewery_id}")

    .form-actions
      button(type="submit" class="btn btn-primary") Save changes

The template first checks the is_create variable. If it’s false, then we’re editing an existing beer, and the caption is filled with that name. Otherwise, it’s titled as Create Beer.

Creating Beers

Creating beers is largely the same as editing beers:

beer_app.js (create beer):

function begin_create_beer(req, res) {
    var view = { is_create : true, beer:{
      type: '',
      name: '',
      description: '',
      style: '',
      category: '',
      abv: '',
      ibu: '',
      srm: '',
      upc: '',
      brewery_id: ''
    } };
    res.render('beer/edit', view);
  }
  function done_create_beer(req, res) {
    var doc = normalize_beer_fields(req.body);
    var beer_id = doc.brewery_id + '-' +
                  doc.name.replace(' ', '-').toLowerCase();
    db.add( beer_id, doc, function(err, result) {
      if (err) throw err;
      res.redirect('/beers/show/'+beer_id);
    });
  }
  app.get('/beers/create', begin_create_beer);
  app.post('/beers/create', done_create_beer);

Here we display the same form as the one for editing beers, except we set the is_create parameter to true, and pass an empty beer object. This is necessary because the template still tries to populate the form fields with existing values.

In the POST handler, we call normalize_beer_field as above when editing beers.

Because we’re creating a new beer, we use the add method instead. This raise will cause the callback to be invoked with an error if the beer already exists. We catch this and display it to the user.

If everything went well, the user is redirected to the beer display page for the newly created beer.

Searching Beers

In the beer listing page above, you might have noticed a search box at the top. We can use it to dynamically filter our table based on user input. We’ll use Javascript at the client layer to perform the querying and filtering, and views with range queries at the server (nodejs/express) layer to return the results.

Before we implement the server-side search method, we need to put the following in the static/js/beersample.js file (if it’s not there already) to listen on search box changes and update the table with the resulting JSON (which is returned from the search method):

static/js/beersample.js (snippet):

$(document).ready(function() {

    /**
     * AJAX Beer Search Filter
     */
    $("#beer-search").keyup(function() {
       var content = $("#beer-search").val();
       if(content.length >= 0) {
           $.getJSON("/beers/search", {"value": content}, function(data) {
               $("#beer-table tbody tr").remove();
               for(var i=0;i<data.length;i++) {
                   var html = "<tr>";
                   html += "<td><a href=\"/beers/show/"+data[i].id+"\">"+data[i].name+"</a></td>";
                   html += "<td><a href=\"/breweries/show/"+data[i].brewery+"\">To Brewery</a></td>";
                   html += "<td>";
                   html += "<a class=\"btn btn-small btn-warning\" href=\"/beers/edit/"+data[i].id+"\">Edit</a>\n";
                   html += "<a class=\"btn btn-small btn-danger\" href=\"/beers/delete/"+data[i].id+"\">Delete</a>";
                   html += "</td>";
                   html += "</tr>";
                   $("#beer-table tbody").append(html);
               }
           });
       }
    });
});

The code waits for keyup events on the search field, and if they happen, it issues an AJAX query on the search function within the app. The search handler computes the result (using views) and returns it as JSON. The JavaScript then clears the table, iterates over the results, and creates new rows.

The search handler looks like this:

beer_app.js (ajax search response):

function search_beer(req, res) {
    var value = req.query.value;
    var q = { startkey : value,
              endkey : value + JSON.parse('"\u0FFF"'),
              stale : false,
              limit : ENTRIES_PER_PAGE }
    db.view( "beer", "by_name", q).query(function(err, values) {
      var keys = _.pluck(values, 'id');
      db.getMulti( keys, null, function(err, results) {
        var beers = [];
        for(var k in results) {
          beers.push({
            'id': k,
            'name': results[k].value.name,
            'brewery_id': results[k].value.brewery_id
          });
        }

        res.send(beers);
      });
    });
  };
  app.get('/beers/search', search_beer);

The beer_search function first extracts the user input by examining the query string from the request.

It then builds an options object which will be passed to the view query api. We pass the users input for the startkey, and pass the users input appended with a Unicode \u0FFF, which for the view engine means “end here.” You need to get used to it a bit, but it’s actually very neat and efficient.

We use getMulti (as has been seen in list beers page in a previous tutorial section) to retrieve the complete data for each beer. However, unlike before, rather than rendering a template using the data we have retrieved, we send the object directly to express, which will kindly serialize it to JSON for us.

Now your search box should work nicely.

Wrapping Up

While you may notice that the breweries management code has been implemented in the repository, it is left as an exercise to the user to work out the details of this. Additionally, here are some things that are not implemented in the example which you might add while learning to use the SDK:

  • When deleting a brewery, ensure it has no beers dependent on it.

  • Provide a search where one can query beers belonging to a given brewery.

  • Handle concurrent updates to a beer and/or brewery.

  • Implement a like feature, where one can like a beer or a brewery; likewise, they can unlike one as well!

The tutorial presents an easy approach to start a web application with Couchbase Server as the underlying data source. If you want to dig a little bit deeper, the full source code in the couchbaselabs repository on GitHub has more code to learn from. This code might be extended and updated from time to time.

Advanced Usage

Replica Reads

In addition to the default methods for you to retrieve documents from the cluster (get), we additionally provide the ability to request documents from replica sets (via the getReplica and getReplicaMulti functions). This allows you to gracefully handle instances where the active node holding your document goes offline, but accessing the document from a replica node may still be acceptable. Keep in mind that doing storage operations directly to replica nodes is not possible. Additionally, replica-reads are not meant to provide load balancing capabilities, the cluster handles mapping of keys to individual nodes to provide load balancing across all available nodes.

Note that retrieving data from a replica node could return potentially stale document contents if the replica did not receive a newer mutation of the document.

The replica read function works nearly identically to a normal get request, though it does not provide the ability to do getAndTouch operations. Additionally, replica-reads allow you to specify which replica you wish to retrieve the document from which can be any number from 0 to num_replicas-1, you may also specify -1 (the default) to retrieve from the fastest responding replica node.

Bulk Loading

Most API functions have both single and multi-key (batched) variants. The batched variant has the same name as the single-key variant, but its method name has Multi appended to it.

The batched operations are significantly quicker and more efficient, especially when dealing with many small values, because they allow pipelining of requests and responses, saving on network latency.

Batched operations tend to accept an array of keys and return an object where the object key matches the key requested and the value of said keys are the same as if a singular operation was done.

Errors during Batched Operations

Sometimes a single (or many) keys in a batched operation will fail. In this case, the callback will receive an error parameter containing 1 (rather than the usual 0), and the keys that failed will have a result object which contains an error key containing an error object rather than value key which would normally contain the value that was retrieved.

Logging & Debugging

This section explains how to uncover bugs in your application (or in the SDK itself).

Components

To debug anything, you must be able to identify in which domain a problem is found. Specifically, the following components participate in typical Couchbase operations:

  • Couchbase Server

    This is the server itself, which stores your data. Errors can happen here if your data does not exist, or if there are connectivity issues with one or more nodes in the server. Note that while Couchbase Server is scalable and fault tolerant, there are naturally some conditions that would cause failures (for example, if all nodes are unreachable).

  • libcouchbase

    This is the underlying layer that handles network communication and protocol handling between a client and a Couchbase node. Network connectivity issues tend to happen here.

  • Node.js C++ Binding Layer

    This is the C++ code that provides the bulk of the SDK. It interfaces with the libcouchbase component, handles marshaling of information between libcouchbase and your application, performs input validation, and encoding and decoding of keys and values.

  • Node.js Layer

    This is written in pure Javascript. For simple key-value operations these normally just dispatch to the C++ layer. Most of the view operations are handled here as well, with the C++ layer just performing the lower level network handling.

Using the APIs

Data types with Node.js

All Javascript data types that are able to be serialized via the JSON functions are supported by default. It is worth noting that recursive structures cannot be serialized. It is also possible to encode data using the raw Node.js formats or in UTF-8 as well by using the format option on the storage operations.

bucket.set(‘test-key’, 'test-value', {
  format:couchbase.format.utf8
}, function(err, result) {
  console.log(result);
});

Callback and MultiCallback Format

The Node.js Couchbase driver employs a callback pattern to notify the application when results are ready. These callbacks are passed to the operation methods and the callbacks are invoked later once the results or errors are ready. All storage and retrieval operations follow the same callback pattern and have the following parameters:

  • error

    For singular operations, this parameter will contain an Error object representing any error that occurred during the execution of the operation, or alternatively will contain null if no errors occurred.

    For a batch operation, this will contain either 1 or 0 representing if an error occurred on any of the batched operations.

  • result/results

    For a singular operation, this parameter will contain an object with the properties as listed below.

    For a batched operation, this will be an object where the keys match the keys your operation targeted, and the values will contain an object containing the fields listed below.

    • value

      This will contain the value of the key that was requested if the operation was a retrieval type operation.

    • error

      This will contain an Error object representing any errors that occurred during execution of this particular batch operation.

    • cas

      This will contain an opaque object representing the resulting CAS value of the key that was operated upon. This value is not meant to be user-facing, but should be passed directly to other operations for locking purposes.

Concurrency with CAS and Locking

In production deployments, it is possible that you will have more than a single instance of your application trying to modify the same key. In this case a race condition happens in which a modification one instance has made is immediately overridden.

Consider this code:

function add_friend(user_id, friend_id) {
  bucket.get('user_id-' + user_id, function(err, result) {
    result.value.friends[friend_id] = { 'added': new Date() };
    bucket.set('user_id-' + user_id, result.value, function(err, result) { } );
  });
}

In this case, friends is an array of friends the user has added, with the keys being the friend IDs, and the values being the time when they were added.

When the friend has been added to the array, the document is stored again on the server.

Assume that two users add the same friend at the same time, in this case there is a race condition where one version of the friends array ultimately wins.

Couchbase provides two means by which to solve for this problem. The first is called Opportunistic Locking and the second is called Pessimistic Locking.

Both forms of locking involve using a CAS value. This value indicates the state of a document at a specific time. Whenever a document is modified, this value changes. The contents of this value are not significant to the application, however it can be used to ensure consistency. You can pass the CAS of the value as it is known to the application and have the server make the operation fail if the current (server-side) CAS value differs.

Error Handling

When something goes wrong, an Error object will be returned into the callback passed to the operation. This Error object contains a lot of information that can be helpful to identify what went wrong. Logging this object will typically contain a stack trace as well as contain the error code and message that was generated within libcouchbase.

An Error object contains all of the default Javascript error properties, but is also enhanced with a numeric code value which is represents the error encountered within libcouchbase. This error code can be used for quick identification of errors and easier error handling.

API Reference

The API reference for the Node.js client is available at http://www.couchbase.com/autodocs/couchbase-node-client-latest/index.html

Release Notes

The following sections provide release notes for individual release versions of Couchbase Node.js Client Library. To browse or submit new issues, see Couchbase Node.js Client Library Issues Tracker.

Release Notes for Couchbase Node.js Client Library 1.2.4 GA (11 May 2014)

New Features and Behavior Changes in 1.2.4

  • Updated embedded libcouchbase to 2.3.1.
  • Added support for specifying hosts via SRV record (experimental).
  • Fixed SmartOS support.
  • Added additional error details for build issues.
  • Various minor bug, documentation, and test fixes.

Release Notes for Couchbase Node.js Client Library 1.2.1 GA (4 February 2014)

New Features and Behavior Changes in 1.2.1

  • Added a mock version of Couchnode for easier testing.
  • Made total_rows available from view requests.
  • Added initial experimental N1QL querying support.
  • Minor internal refactoring and bug fixes.

Release Notes for Couchbase Node.js Client Library 1.2.0 GA (3 December 2013)

New Features and Behavior Changes in 1.2.0

  • Added missing touchMulti method.
  • Added missing arithmeticMulti and removed broken incrMulti and decrMulti.
  • Added support for Node.js v0.11.

Release Notes for Couchbase Node.js Client Library 1.1.1 GA (10 November 2013)

New Features and Behavior Changes in 1.1.1

  • Various minor bug fixes.
  • Corrected issue with http requests.
  • Significant updates to docs.
  • Embedded libcouchbase 2.2.0

Release Notes for Couchbase Node.js Client Library 1.1.0 GA (5 November 2013)

New Features and Behavior Changes in 1.1.0

  • Various bug fixes.
  • Added support for replica reads.
  • Removed dependency on libcouchbase in favor of an embedded version to simplify installation and management complexity.

Release Notes for Couchbase Node.js Client Library 1.0.1 GA (1 October 2013)

New Features and Behavior Changes in 1.0.1

  • Various bug fixes.
  • Added support for specifying hash keys.
  • Updated all tests to use the Mocha testing framework.
  • Corrected issue with design document management functions.

Release Notes for Couchbase Node.js Client Library 1.0.0 GA (12 September 2013)

New Features and Behavior Changes in 1.0.0

  • Various bug fixes.

Known Issues in 1.0.0

  • Durability requirements on storage operations might fail and could time out even though the durability requirements were successfully verified.

Release Notes for Couchbase Node.js Client Library 1.0.0 Beta (6 September 2013)

New Features and Behavior Changes in 1.0.0-beta

  • Implemented ability to specify durability requirements to all storage operations.
  • Fixed various minor bugs.

Release Notes for Couchbase Node.js Client Library 0.1.0 GA (30 August 2013)

New Features and Behavior Changes in 0.1.0

  • Complete rewrite of the 0.0.x series of couchnode.
  • Brand new libcouchbase binding layer, which brings huge performance and stability benefits.
  • Refactored user-facing API to be cleaner and easier to use.

Licenses

This documentation and associated software is subject to the following licenses.

Documentation License

This documentation in any form, software or printed matter, contains proprietary information that is the exclusive property of Couchbase. Your access to and use of this material is subject to the terms and conditions of your Couchbase Software License and Service Agreement, which has been executed and with which you agree to comply. This document and information contained herein may not be disclosed, copied, reproduced, or distributed to anyone outside Couchbase without prior written consent of Couchbase or as specifically provided below. This document is not part of your license agreement nor can it be incorporated into any contractual agreement with Couchbase or its subsidiaries or affiliates.

Use of this documentation is subject to the following terms:

You may create a printed copy of this documentation solely for your own personal use. Conversion to other formats is allowed as long as the actual content is not altered or edited in any way. You shall not publish or distribute this documentation in any form or on any media, except if you distribute the documentation in a manner similar to how Couchbase disseminates it (that is, electronically for download on a Web site with the software) or on a CD-ROM or similar medium, provided however that the documentation is disseminated together with the software on the same medium. Any other use, such as any dissemination of printed copies or use of this documentation, in whole or in part, in another publication, requires the prior written consent from an authorized representative of Couchbase. Couchbase and/or its affiliates reserve any and all rights to this documentation not expressly granted above.

This documentation may provide access to or information on content, products, and services from third parties. Couchbase Inc. and its affiliates are not responsible for and expressly disclaim all warranties of any kind with respect to third-party content, products, and services. Couchbase Inc. and its affiliates will not be responsible for any loss, costs, or damages incurred due to your access to or use of third-party content, products, or services.

The information contained herein is subject to change without notice and is not warranted to be error free. If you find any errors, please report them to us in writing.