justjs: node.js tutorials

New here? You might want to start at the beginning.

5/18 '12

Classy routes with Express

Edit: brought back the notFound() function so we can use it when a reasonable-looking URL doesn't match an actual post.

When We Last Saw Our Heroes...

In the last installment, we built our first node app. And we learned that every node app is a webserver, deciding what to do with every incoming request from a browser entirely under its own power. Which is pretty cool actually.

But it can also get messy.

Consider this code from the previous example:

var server = http.createServer(function(req, res) {
  if (req.url === '/')
  {
    // Deliver a list of posts
    index();
  }
  else if (posts[req.url])
  {
    // Deliver this particular post
    post(req.url);
  }
  else
  {
    notFound();
  }
});

Here we have the beginnings of what could easily become a very, very long "if... else if... else if... else if... else if... [deep breath]... else" block. Those can easily become difficult to maintain. What happens when our app is a little less trivial and we have 100 different actions to deal with?

Fortunately, there's Express! Express is an optional Node module that bills itself as a web framework, and it offers many things: user sessions, templates, file downloads, parsing of form submissions, routes (which will solve our problem today), and more. I'm not 100% sold on the way Express solves every problem. But this is Node, and in Node development you are rarely forced to use anything you don't want. So we'll pick and choose the parts of Express that help us.

Adding Express to your Project

Let's get started on our second version of the blog. To begin, copy your blog-1 folder to blog-2. Then, in the Terminal window, use the cd command to enter the blog-2 folder. Next use the npm package manager (the "Node package manager") to install Express in that project:

cd blog-2

npm install express

When this command completes, you should be able to type:

ls node_modules

And see:

express

The npm package manager does a great job of installing optional packages for you and knows how to install the additional components each package depends on. Later we'll see how to create a packages.json file so that the requirements of our project can be automatically installed.

Now fire up your text editor. Create a server.js file in the blog-2 folder, then copy and paste your previous version from the blog-1 folder to get started.

Express is a Node module. As we saw previously, Node modules can be loaded with the require function.

Express extends Node's concept of a webserver, so we don't have to require the http module anymore. So we'll replace this line:

var http = require('http');

With this one:

var express = require('express');

Our Express web application serves the same purpose that our HTTP server object formerly did. So we can remove this line:

var server = http.createServer(function(req, res) { ... code goes here ... } );

... And replace it with this line, which creates an Express "application:"

var app = require('express').createServer();

But what about all that code inside our callback function? Don't worry, we'll bring that back! Rather than writing a giant if/else if/else if/else block, Express lets us register "routes" for each action of the web application. For every distinct action the user can take on the website, we'll just register a route.

Here's what happens when we take the index() function from the first version of the blog and turn it into an Express route:

// Deliver a list of posts when we see just '/'
app.get('/', function(req, res) {
  var s = "<title>My Blog</title>\n";
  s += "<h1>My Blog</h1>\n";
  s += "<ul>\n";
  for (var slug in posts)
  {
    var post = posts[slug];
    s += '<li><a href="/posts/' + slug + '">' + post.title + '</a></li>' + "\n";
  }
  s += "</ul>\n";
  res.send(s);
});

Here are four interesting things about this code:

1. Our index() function turned into a callback function, passed to the route function of the app object. We could also have just passed the function by name, but now that we're registering each route separately it doesn't really help the readability of the code as much as it did before to give each function a name.

2. We don't have to say "if req.url equals blah blah blah..." to figure out what to do. Instead we just tell Express we want a route that matches '/'.

3. We don't need our own sendBody function to set the content type to text/html before sending the page. Express has enhanced the res object for us with a shiny new send method that does the same thing.

4. I changed the link to each individual post a little bit, prefixing it with /posts/. I did that because otherwise we've painted ourselves into a corner: if someone writes a post called "last", we can't add an action called "/last" later without a conflict. It's always a good idea to keep your URL "namespace" clean so that conflicts like this won't bite you later.

"Hang on a minute! You wrote app.get(). But you're not getting something, you're setting up a route. This is very confusing."

Yeah, it is at first. Just keep in mind that what you're really doing is setting a route that responds to the GET HTTP method (the method used for plain old ordinary visits to webpages), as opposed to the POST HTTP method (used for submitting forms). I agree that "app.get actually SETS stuff" is confusing, but it will feel more natural to you after the first time you write"app.post".

Matching Parameters in Routes

So far, so good. But this first route is pretty trivial. What about delivering individual posts?

Here's the route for posts:

// Deliver a specific post when we see /posts/
app.get('/posts/:slug', function(req, res) {
  var post = posts[req.params.slug];
  if (typeof(post) === 'undefined')
  {
    notFound(res);
    return;
  }
  var s = "<title>" + post.title + "</title>\n";
  s += "<h1>My Blog</h1>\n";
  s += "<h2>" + post.title + "</h2>\n";
  s += post.body;
  res.send(s);
});

Now this is pretty nifty. Express provides us with a handy way to match all URLs that begin with /posts/ and are followed by additional characters corresponding to the placeholder :slug (the slug, or unique URL, of the post). And those additional characters automatically appear to our code as req.params.slug.

Did you catch that? Any component of the URL in your route that you prefix with a : will be captured and automatically become available in the req.params variable. That is very cool, and saves us a lot of monkeying around.

For this route to work we do need to make a small change to our data. In the first version of the app, the slug of every post started with a /. Now the / is part of what has already been matched by the route:

/posts/:slug

See that second /?

So let's clean up our posts array, removing the leading slashes from the properties:

var posts = {
  'welcome-to-my-blog': {
    title: 'Welcome to my blog!',
    body: 'I am so glad you came.'
  },
  'i-am-concerned-about-stuff': {
    title: 'I am concerned about stuff!',
    body: 'People need to be more careful with stuff.'
  },
  'i-often-dream-of-trains': {
    title: 'I often dream of trains.',
    body: "I often dream of trains when I'm alone."
  }
};

We're almost finished! Two more things. First, let's create a route to catch nonexistent URLs and call the notFound function. We'll use the * wildcard, which matches anything after that point in the URL.

app.get('*', function(req, res) {
  notFound(res);
});

It's very important to register this route last. The wildcard matches everything, so if you put it first, everything on the site yields a 404 error.

We'll also touch up our notFound function a little bit to send the right status code while still taking advantage of the simplicity of res.send:

function notFound(res)
{
  res.send('<h1>Page not found.</h1>', 404);
}

Notice that we have added a second argument when calling res.send to signify that we are sending a 404 error. This is important because it allows Google and other search engines to distinguish this from an interesting page that should be indexed.

One last detail: listening for connections. Previously we did this with the server variable, but we've replaced that with the app variable. So we write:

app.listen(3000);

Now you're ready to fire up the app again. Make sure you are in the blog-2 directory (via the cd command), then type:

node server.js

Now you can connect to your app:

http://localhost:3000/

You'll see the same functionality as before, except that the URLs for posts have changed a bit.

As always, you can cheat and pick up the source code on github.

Refactor, Rinse, Repeat!

Sometimes the best feature you can add to a project today is not a feature. It's a refactoring of your code to make it more readable and maintainable, in order to accommodate the next feature better.

But speaking of the next feature, In our next installment we'll start creating new blog posts and storing them in a database, making our blog 333% more real!

 

blog comments powered by Disqus