Node Tutorial Part 3

15 Nov 2010 | By Alex Young | Tags server node tutorials lmawa nodepad

Welcome to part 3 of Let’s Make a Web App, a tutorial series about building a web app with Node. This series will walk you through the major areas you’ll need to face when building your own applications. These tutorials are tagged with lmawa.

Previous tutorials:

In this part we will build on last week’s skeleton app. I already added a simple Document model, so let’s flesh that out a bit. These tutorials expect you to check out the code from the git repository, so visit nodepad to get it.

Logging

Let’s add some logging. Express has a logger, and it can be configured in the app.configure block. Just make sure you use it:

app.configure(function() {
  app.use(express.logger());
  // Last week's configure options go here
});

It’s usually a good idea to configure logging slightly differently, depending on environment. I’ve set it up the same way for now:

app.configure('development', function() {
  app.use(express.logger());
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 
});

app.configure('production', function() {
  app.use(express.logger());
  app.use(express.errorHandler()); 
});

API

We can model accessing documents over HTTP using a CRUD-based (Create, Read, Update and Delete) RESTful API:

  • GET /documents – Index method that returns a document list
  • POST /documents/ – Create a new document
  • GET /documents/:id – Returns a document
  • PUT /documents/:id – Update a document
  • DELETE /documents/:id – Delete a document

The HTTP verbs are important — notice that the index and create methods have the same URL, but respond differently depending on whether a HTTP GET or PUT is used. Express will route these to the appropriate methods.

HTTP Verbs are Significant

If you’re not used to working this way just remember that the HTTP verb is significant. For example, last week we defined this method:

app.get('/', function(req, res) {
  // Respond to GET for '/'
  // ...
});

If you write a form that posts to the same URL, Express will return an error because no route has been set up.

Also recall from last week that we added express.methodOverride to the configuration options. The reason for this was we can’t rely on browsers understanding HTTP verbs like DELETE, but we can use a convention to get around the problem — forms can use hidden variables that Express can interpret as a “real” HTTP method.

In some ways this approach to RESTful HTTP APIs might seem inelegant, but the advantage of using these conventions is a lot of web applications fit this approach.

CRUD Stub Reference

For reference, this is what a stubbed out CRUD set of routes looks like:

// List
app.get('/documents.:format', function(req, res) {
});

// Create 
app.post('/documents.:format?', function(req, res) {
});

// Read
app.get('/documents/:id.:format?', function(req, res) {
});

// Update
app.put('/documents/:id.:format?', function(req, res) {
});

// Delete
app.del('/documents/:id.:format?', function(req, res) {
});

Notice that Express uses del instead of delete.

Asynchronous Databases

Before we start writing each REST method, let’s look at an example: loading a list of documents. You’re probably used to working like this:

app.get('/documents', function(req, res) {
  var documents = Document.find().all();

  // Send the result as JSON
  res.send(documents);

We generally use database libraries asynchronously in Node. That means we need to do this:

app.get('/documents', function(req, res) {
  Document.find().all(function(documents) {
    // 'documents' will contain all of the documents returned by the query
    res.send(documents.map(function(d) {
      // Return a useful representation of the object that res.send() can send as JSON
      return d.__doc;
    }));
  });
});

The difference is a callback is used to access the results. This example isn’t particularly efficient, because it loads every single document into an array — it may be better to stream the results to the client as they become available.

Formats

I’d like to support HTML and JSON, where appropriate. The following pattern could be used:

// :format can be json or html
app.get('/documents.:format?', function(req, res) {
  // Some kind of Mongo query/update
  Document.find().all(function(documents) {
    switch (req.params.format) {
      // When json, generate suitable data
      case 'json':
        res.send(documents.map(function(d) {
          return d.__doc;
        }));
      break;

      // Else render a database template (this isn't ready yet)
      default:
        res.render('documents/index.jade');
    }
  });
});

This illustrates a load of core Express/Connect functionality: the routing string uses :format to detect if the client wants JSON or HTML. The question mark indicates that the format can be omitted.

Notice that this pattern wraps the database operation around the actual responder code. A similar pattern can be used for saving or deleting items.

Redirection

The create document method returns JSON documents, or redirects the client when HTML is requested:

app.post('/documents.:format?', function(req, res) {
  var document = new Document(req.body['document']);
  document.save(function() {
    switch (req.params.format) {
      case 'json':
        res.send(document.__doc);
       break;

       default:
        res.redirect('/documents');
    }
  });
});

This uses res.redirect to redirect the browser back to the document list. It could redirect to the edit form just as easily. We’ll take a closer look at this when we add the user interface.

Tests

I usually start building apps like this by testing the API. It’s easier to get a lot of this work done before embarking on client-side code. The first thing to do is add a database connection for test databases:

app.configure('test', function() {
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
  db = mongoose.connect('mongodb://localhost/nodepad-test');
});

Then in test/app.test.js, I’m forcing the test environment:

process.env.NODE_ENV = 'test';

That means the test database can be safely trashed.

The tests themselves take a bit of getting used to. Expresso tests work well for testing Express applications, but figuring out the finer details took serious source code reading and mailing list research.

Here’s a revealing example:

  'POST /documents.json': function(assert) {
    assert.response(app, {
        url: '/documents.json',
        method: 'POST',
        data: JSON.stringify({ document: { title: 'Test' } }),
        headers: { 'Content-Type': 'application/json' }
      }, {
        status: 200,
        headers: { 'Content-Type': 'application/json' }
      },

      function(res) {
        var document = JSON.parse(res.body);
        assert.equal('Test', document.title);
      });
  }

The name of the test, 'POST /documents.json', could be anything. The framework doesn’t actually parse these. The request is defined in the first set of parameters. In this case, I specify the Content-Type header. If these aren’t set to the appropriate type, the Connect middleware won’t be able to parse the data.

I specifically wrote a test for JSON and application/x-www-form-urlencoded because readers are likely to get stumped on this in their own code. Just remember that out of the box, Express doesn’t automatically deal with encoded form data, which is why we set up methodOverride in the configuration block.

Refer to commit 39e66cb to see these test examples.

Conclusion

You should now understand how to:

  • Stub CRUD methods with appropriate HTTP verbs in Express
  • Structure apps that can be tested using Express, Expresso, and Mongoose
  • Write simple Expresso tests

Next week I’ll finish off the document API methods and start adding some basic HTML templates. I intend to add a jQuery-based interface that will melt your faces off, but it’s best if we get the tests and API sorted out first.

References


blog comments powered by Disqus