DailyJS

Node Tutorial Part 3

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials server node lmawa nodepad

Node Tutorial Part 3

Posted by Alex R. Young on .
Featured

tutorials server node lmawa nodepad

Node Tutorial Part 3

Posted by Alex R. Young on .

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