Node.js, Part III: Full Stack Application

Posted on by in Development

In my previous posts, I introduced you to Node.js and walked through a bit of its codebase. Now I want to get a simple, but non-trivial Node.js application running. My biggest problem with Node.js so far has been the lack of substantial examples: if I see one more Hello World or Echo Server, I’ll flip my lid (side note: I found the same thing for Ruby’s EventMachine so I created my evented repo). By far the best resource I’ve found for learning Node.js has been DailyJS, highly recommended!

So I’ve spent the last few weeks building Chrono.js, an application metrics server. Chrono is still under development so it’s really not appropriate for general usage but it makes for a decent example app.

Sample Request

Here’s a basic request which fetches data from MongoDB and renders it via jqtpl:

app.get('/', function(req, res) {
  db.open(function(err, ignored) {
    if (err) console.log(err);
    db.collectionNames(function(err, names) {
      if (err) console.log(err);
      db.close();
      pro = _(names).chain()
        .pluck('name')
        .map(function(x) { return x.substr(x.indexOf('.') + 1); })
        .reject(function(x) { return (x.indexOf('system.') == 0); })
        .value();
      res.render('index.html.jqtpl', {
        locals: {
          metrics: pro
        }
      });
    });
  });
});

We are scanning MongoDB for the set of metric collections and displaying them in the selector so the user can switch between them. See the main source file for more examples about parameter handling, headers, JSON, etc.

Installation

You will need MongoDB installed and running locally. You should already have Node.js and npm installed from Part I. Let’s install the javascript libraries required and Chrono itself:

npm install express expresso jqtpl mongodb underscore tobi
git clone git://github.com/mperham/chrono.js.git

Express is a lightweight application server, similar to Sinatra. jqtpl is jQuery Templates, which allows us to dynamically render HTML. mongodb is the MongoDB client driver for Node.js. expresso is a TDD framework and tobi is an HTTP functional testing package. Finally, underscore is a great little utility library which provides JavaScript with a much more sane functional programming API.

Running

You can run the tests:

expresso

or run the Chrono.js server:

node chrono.js

Click http://localhost:3000/load/registrations to load in some fake data and visit http://localhost:3000 (select ‘registrations’) to see the results, graphed in living color for you!

Testing

Expresso gives us a simple DSL for testing our endpoints but I found it to be lacking basic setup/teardown functionality which I had to roll myself in order to insert some test data. I would have preferred to use vows.js but it appears to be incompatible with tobi. Check out the Chrono.js test suite for what I came up with, here’s a small sample:

  exports['POST /metrics'] = function() {
    assert.response(app,
      { url: '/metrics', method: 'POST', headers: { 'content-type': 'application/json' }, data: JSON.stringify({ k: 'registrations', v: 4, at: parseInt(Number(new Date())/1000) }) },
      { status: 201, headers: { 'Content-Type': 'text/html; charset=utf8' }, body: '' }
    );
  }

With expresso, we export the set of functions to run from our test module. Expresso runs them all in parallel and collects the results. The parallelism means that you must be careful with any test data you create. Since MongoDB doesn’t support transactions, we can’t use transactions to isolate each of our tests (e.g. see Rails’s transactional fixtures) so you need to be careful about the data created or deleted by each test and how you assert the current state.

Conclusion

While I’ve gotten much better in the last week, I’ll admit I’m still uncomfortable with Node.js. Its asynchronous semantics mean that your code runs as part of a chain of callbacks; knowing when, where and how that chain works is still difficult for me to understand. Frequently you’ll just get a black screen and a process that won’t exit or an error message that may or may not be related to the actual bug.

That said, I still remember being very frustrated with Ruby, its frequent use of “magic” and poor documentation (e.g. navigating this still befuddles me). I overcame those issues to be very comfortable with Ruby in general; I hope more time with Node.js will give me the same relief.