Nodejitsu

Save time managing and deploying your node.js app. Code faster with jitsu and npm

A simple web service in node.js

About the author

Name
Location
Worldwide
nodejitsu nodejitsu

I recently gave a talk at ADICU Devfest 2011 on node.js. The talk was aimed at Computer Science students who did not know anything about node.js and more importantly, how to get started building simple and elegant web services from scratch. The slides (and code) from my talk are available on GitHub.

This article will walk through the code that I presented to build a simple RESTful bookmarking API using node.js and:


  • Journey: A liberal JSON-only HTTP request router for node.js
  • Cradle: A high-level, caching, CouchDB library for Node.js
  • Winston: A multi-transport async logging library for node.js
  • Optimist: Light-weight option parsing for node.js


Getting Started

The first step to doing anything with node.js is creating a server to accept incoming HTTP requests. You can also create a TCP server or work with UDP, but I won't be going into that here. So what does such a server look like?

var http = require('http'),  
    winston = require('winston');

/**
 * Creates the server for the pinpoint web service
 * @param {int} port: Port for the server to run on
 */
exports.createServer = function (port) {  
  var server = http.createServer(function (request, response) {
    var data = '';

    winston.info('Incoming Request', { url: request.url });

    request.on('data', function (chunk) {
      data += chunk;
    });

    response.writeHead(501, { 'Content-Type': 'application/json' });
    response.end(JSON.stringify({ message: 'not implemented' }));
  });

  if (port) {
    server.listen(port);
  }

  return server;
};

The above example is simple (almost trivial): it exports a server that reads the request body, logs the request using winston, and responds with the appropriate HTTP response code (501 - Not Implemented). Not very exciting but enough to get us started.


Running the development server

The development server lives in bin\server and has been configured so that we can run the development server at the various stages of development outlined in this tutorial. Running it is simple, just remember to pass the -t argument which specifies which directory under /lib to use for the server:

  • 00getting-started: The full source code from 'Getting Started'
  • 01routing: The full source code from 'Adding some Routes'
  • 02couchdb: The full source code from 'Interacting with CouchDB'
  • 03authentication: The full source code from 'Adding HTTP Basic Auth'
  $ bin/server -t 00getting-started
  Pinpoint demo server listening for 00getting-started on http://127.0.0.1:8000
  4 Feb 17:59:22 - info: Incoming Request url=/

Adding some Routes

Now that we have a server that tells the world we haven't done anything it's time to think about what our application does:

  • List: GET to /bookmarks should respond with a list of bookmarks
  • Create: POST to /bookmarks should create a new bookmark
  • Show: GET to /bookmarks/:id should respond with a specific bookmark
  • Update: PUT to /bookmarks/:id should update a specific bookmark
  • Destroy: DELETE to /bookmarks/:id should delete a specific bookmark

Pretty simple right? Absolutely. To accomplish this routing tasks we are going to use Journey. Journey is a great library with a lot of features. 0.3.1 was just released, which we will be taking advantage of in this post.

exports.createRouter = function () {  
  return new (journey.Router)(function (map) {
    map.path(/\/bookmarks/, function () {
      //
      // LIST: GET to /bookmarks lists all bookmarks
      //
      this.get().bind(function (res) {
        res.send(501, {}, { action: 'list' });
      });

      //
      // SHOW: GET to /bookmarks/:id shows the details of a specific bookmark 
      //
      this.get(/\/([\w|\d|\-|\_]+)/).bind(function (res, id) {
        res.send(501, {}, { action: 'show' });
      });

      //
      // CREATE: POST to /bookmarks creates a new bookmark
      //
      this.post().bind(function (res, bookmark) {
        res.send(501, {}, { action: 'create' });
      });

      //
      // UPDATE: PUT to /bookmarks updates an existing bookmark
      //
      this.put(/\/([\w|\d|\-|\_]+)/).bind(function (res, bookmark) {
        res.send(501, {}, { action: 'update' });
      });

      //
      // DELETE: DELETE to /bookmarks/:id deletes a specific bookmark
      //
      this.del(/\/([\w|\d|\-|\_]+)/).bind(function (res, id) {
        res.send(501, {}, { action: 'delete' });
      });
    });
  }, { strict: false });
};

The above code generates a Journey router that matches the routes we outlined using regular expressions. In each route, we respond with 501 Not Implemented and the corresponding action so that we can be sure we've hit the correct route.

We use the map.path syntax to scope the subsequent routes behind /bookmarks. The changes that need to be made to our development server are minimal. We just need to add code to create our router and later it to route our request with the associated request body within our HTTP server:

request.on('end', function () {  
  //
  // Dispatch the request to the router
  //
  router.route(request, body, function (route) {
    response.writeHead(route.status, route.headers);
    response.end(route.body);
  });
});

You can view the entire file on GitHub here.


Testing Routes using http-console

One of the things I did differently in this demo than I do in my own projects is that there are no vowsjs tests. I choose not to include tests in this demo because the additional overhead of understanding how a particular test framework works seemed a little high for the complete beginner.

The alternative was to use http-console: a simple, intuitive HTTP REPL. Getting http-console is easy to install using npm:

  [sudo] npm install http-console

So lets fire-up http-console for an interactive session a couple of our newly minted routes:

  $ http-console http://127.0.0.1:8000
  > http-console 0.5.1
  > Welcome, enter \help if you're lost.
  > Connecting to 127.0.0.1 on port 8000.

  http://127.0.0.1:8000/> GET /bookmarks
  HTTP/1.1 501 Not Implemented
  Date: Sat, 05 Feb 2011 02:57:46 GMT
  Server: journey/0.3.0
  Content-Type: application/json
  Content-Length: 17
  Connection: close

  { action: 'list' }
  http://127.0.0.1:8000/> GET /bookmarks/foobar
  HTTP/1.1 501 Not Implemented
  Date: Sat, 05 Feb 2011 02:57:52 GMT
  Server: journey/0.3.0
  Content-Type: application/json
  Content-Length: 17
  Connection: close

  { action: 'show' }
  http://127.0.0.1:8000/>   

We made two request to /bookmarks and /bookmarks/foobar respectively and got back 501 in both cases with valid JSON representing the specified action that is not yet implemented. We also got back the application/json header which was automatically set for us by Journey.

Interacting with CouchDB

Interacting with a persistent data store is a must have for any webservice or web application. At Nodejitsu we use CouchDB and our library of choice is cradle. We will define a Bookmark resource with a couple of methods for performing basic CRUD on our bookmark object. Before we get to that we need to configure our Couch with a Design Document for Bookmark resources. If you want to learn more about Design Doucments see CouchDB: The Definitive Guide.

var cradle = require('cradle');

var setup = exports.setup = function (options, callback) {  
  // Set connection configuration
  cradle.setup({
    host: options.host || '127.0.0.1',
    port: 5984,
    options: options.options,
  });

  // Connect to cradle
  var conn = new (cradle.Connection)({ auth: options.auth }),
      db = conn.database(options.database || 'pinpoint-dev');

  if (options.setup) {
    initViews(db, callback);
  }
  else {
    callback(null, db); 
  }
};

var initViews = exports.initViews = function (db, callback) {  
  var designs = [
    {
      '_id': '_design/Bookmark',
      views: {
        all: {
          map: function (doc) { if (doc.resource === 'Bookmark') emit(doc._id, doc) }
        },
        byUrl: {
          map: function (doc) { if (doc.resource === 'Bookmark') { emit(doc.url, doc); } }
        },
        byDate: {
          map: function (doc) { if (doc.resource === 'Bookmark') { emit(doc.date, doc); } }
        }
      }
    }
  ];

  db.save(designs, function (err) {
    if (err) return callback(err);
    callback(null, db);    
  });
};

The above code requires cradle, configures it with the remote host, port and authentication (if required). If options.setup is set then it asynchronously creates the Design Document in CouchDB and later responds with the database connection db.

Now that we have a connection to CouchDB for our application, we can go ahead and create a Bookmark resource that consumes the connection. Within this resource we want to define functions for each of the CRUD operations we've outlined: create, show, list, update and destroy.

/**
* Constructor function for the Bookmark object..
* @constructor
* @param {connection} database: Connection to CouchDB
*/
var Bookmark = exports.Bookmark = function (database) {  
  this.database = database;
};

/**
* Lists all Bookmarks in the database
* @param {function} callback: Callback function
*/
Bookmark.prototype.list = function (callback) {  
  this.database.view('Bookmark/all', function (err, result) {
    if (err) {
      return callback(err);
    }

    callback(null, result.rows.map(function (row) { return row.value }));
  })
};

/**
* Shows details of a particular bookmark
* @param {string} id: ID of the bookmark
* @param {function} callback: Callback function
*/
Bookmark.prototype.show = function (id, callback) {  
  this.database.get(id, function (err, doc) {
    if (err) {
      return callback(err);
    }

    callback(null, doc);
  });
};

/**
* Creates a new bookmark with the specified properties
* @param {object} bookmark: Properties to use for the bookmark
* @param {function} callback: Callback function
*/
Bookmark.prototype.create = function (bookmark, callback) {  
  bookmark._id = helpers.randomString(32);
  bookmark.resource = "Bookmark";

  this.database.save(bookmark._id, bookmark, function (err, res) {
    if (err) {
      return callback(err);
    }

    callback(null, bookmark);
  })
};

/**
* Updates a new bookmark with the specified id and properties
* @param {object} bookmark: Properties to update the bookmark with
* @param {function} callback: Callback function
*/
Bookmark.prototype.update = function (id, bookmark, callback) {  
  this.database.merge(id, bookmark, function (err, res) {
    if (err) {
      return callback(err);
    }

    callback(null, true);
  });
};

/**
* Destroys a bookmark with the specified ID
* @param {string} id: ID of the bookmark to destroy
* @param {function} callback: Callback function
*/
Bookmark.prototype.destroy = function (id, callback) {  
  var self = this;
  this.show(id, function (err, doc) {
    if (err) {
      return callback(err);
    }

    self.database.remove(id, doc._rev, function (err, res) {
      if (err) {
        return callback(err);
      }

      callback(null, true);
    });
  });
};

I won't go into the details of how each of these methods work, but rest assured that they do. It's all very basic usage for cradle, so if you're interested in the specifics I invite you to read the documentation on the cradle GitHub page.

So now that we've configured CouchDB and we have a Bookmark resource, we need to connect our resource to the Journey router that we defined earlier.

exports.createRouter = function (resource) {  
  return new (journey.Router)(function (map) {
    map.path(/\/bookmarks/, function () {
      //
      // LIST: GET to /bookmarks lists all bookmarks
      //
      this.get().bind(function (res) {
        resource.list(function (err, bookmarks) {
          if (err) {
            return res.send(500, {}, { error: err.error });
          }

          res.send(200, {}, { bookmarks: bookmarks });
        });
      });

      //
      // SHOW: GET to /bookmarks/:id shows the details of a specific bookmark 
      //
      this.get(/\/([\w|\d|\-|\_]+)/).bind(function (res, id) {
        resource.show(id, function (err, bookmark) {
          if (err) {
            return res.send(500, {}, { error: err.error });
          }

          res.send(200, {}, { bookmark: bookmark });
        });
      });

      //
      // CREATE: POST to /bookmarks creates a new bookmark
      //
      this.post().bind(function (res, bookmark) {
        resource.create(bookmark, function (err, result) {
          if (err) {
            return res.send(500, {}, { error: err.error });
          }

          res.send(200, {}, { bookmark: result });
        });
      });

      //
      // UPDATE: PUT to /bookmarks updates an existing bookmark
      //
      this.put(/\/([\w|\d|\-|\_]+)/).bind(function (res, id, bookmark) {
        resource.update(id, bookmark, function (err, updated) {
          if (err) {
            return res.send(500, {}, { error: err.error });
          }

          res.send(200, {}, { updated: updated });
        });
      });

      //
      // DELETE: DELETE to /bookmarks/:id deletes a specific bookmark
      //
      this.del(/\/([\w|\d|\-|\_]+)/).bind(function (res, id) {
        resource.destroy(id, function (err, destroyed) {
          if (err) {
            return res.send(500, {}, { error: err.error });
          }

          res.send(200, {}, { destroyed: destroyed });
        });
      });
    });
  }, { strict: false });
};

In each of the new routes, we send the appropriate request data to the Bookmark resource, and asynchronously respond with the appropriate HTTP response code when complete. In the event of an error, we always send 500 Internal Server Error with the error message.


Adding HTTP Basic Auth

The main focus of Journey 0.3.0 was to add a feature where the programmer could specify a filter function that takes the request and body. This filter function will intercept requests before they are passed to any route handler. If the filter function returns a pre-defined Journey error, the router will short-circuit and respond with the status code. We will use this feature to define a filter function that performs HTTP Basic Auth.

var auth = exports.auth = {  
  username: 'admin',
  password: 'password',
  basicAuth: function (request, body, callback) {
    var realm = "Authorization Required",
        authorization = request.headers.authorization;

    if (!authorization) {
      return callback(new journey.NotAuthorized("Authorization header is required."));
    }

    var parts       = authorization.split(" "),           // Basic salkd787&u34n=
        scheme      = parts[0],                           // Basic
        credentials = base64.decode(parts[1]).split(":"); // admin:password

    if (scheme !== "Basic") {
      return callback(new journey.NotAuthorized("Authorization scheme must be 'Basic'"));
    }
    else if(!credentials[0] && !credentials[1]){
      return callback(new journey.NotAuthorized("Both username and password are required"));
    }
    else if(credentials[0] !== auth.username || credentials[1] !== auth.password) {
      return callback(new journey.NotAuthorized("Invalid username or password"));
    }

    // Respond with no error if username and password match
    callback(null);
  }
};

We can set this method on the Journey router by passing it in the options hash. Any routes that we wish to be behind the authentication filter need to be wrapped in a call to map.filter(function () { ... })

exports.createRouter = function (resource) {  
  return new (journey.Router)(function (map) {
    //
    // Resource: Bookmarks
    //
    map.path(/\/bookmarks/, function () {
      //
      // Authentication: Add a filter() method to perform HTTP Basic Auth
      //
      map.filter(function () {
        //
        // All of the previous routes we had go in here, but they
        // are now behind HTTP Basic Auth
        //
      });
    });
  }, { 
    strict: false,
    filter: helpers.auth.basicAuth 
  });
};

Wrapping up

Now that we have completed our web service, lets fire up http-console for an interactive session with our Bookmark resource.

  $ http-console http://127.0.0.1:8000
  > http-console 0.5.1
  > Welcome, enter \help if you're lost.
  > Connecting to 127.0.0.1 on port 8000.

  http://127.0.0.1:8000/> Authorization: Basic YWRtaW46cGFzc3dvcmQ
  http://127.0.0.1:8000/> \json
  http://127.0.0.1:8000/> \headers
  Accept: */*
  Authorization: Basic YWRtaW46cGFzc3dvcmQ
  Content-Type: application/json
  http://127.0.0.1:8000/> POST /bookmarks
  ... { "url": "http://nodejs.org" }
  HTTP/1.1 200 OK
  Date: Sat, 05 Feb 2011 05:38:47 GMT
  Server: journey/0.3.0
  Content-Type: application/json
  Content-Length: 77
  Connection: close

  {
      bookmark: {
          url: 'http://nodejs.org',
          _id: 'xnIgT8',
          resource: 'Bookmark'
      }
  }
  http://127.0.0.1:8000/> GET /bookmarks/xnIgT8
  HTTP/1.1 200 OK
  Date: Sat, 05 Feb 2011 05:39:01 GMT
  Server: journey/0.3.0
  Content-Type: application/json
  Content-Length: 121
  Connection: close

  {
      bookmark: {
          url: 'http://nodejs.org',
          _id: 'xnIgT8',
          resource: 'Bookmark',
          _rev: '1-cfced13a45a068e95daa04beff562360'
      }
  }
  http://127.0.0.1:8000/> GET /bookmarks
  HTTP/1.1 200 OK
  Date: Sat, 05 Feb 2011 05:39:05 GMT
  Server: journey/0.3.0
  Content-Type: application/json
  Content-Length: 369
  Connection: close

  {
      bookmarks: [
          {
              _id: 'xnIgT8',
              _rev: '1-cfced13a45a068e95daa04beff562360',
              url: 'http://nodejs.org',
              resource: 'Bookmark'
          }
      ]
  }

I hope this has been helpful for those of you looking to get started with node.js. Check out the rest of our blog for more advanced libraries and tutorials. Come back soon for more on the Art of Nodejitsu.