Node.js App Basics - Part 2

03 Sep 2013

Feel free to review the other posts in this series:

==

In this installement, let’s extend our server side to log requests, and to serve some static content.

As a quick refresher, here’s our server.js file from part 1:

//'require' says "pull in this installed package"
var connect = require('connect');

//we're still using Node's built in HTTP server, so let's pull it in.
var http = require('http');

//create our middleware stack (just send a basic message for now):
var app = connect()
    .use(function(request, response){
        response.write('Hello from connect!');
        response.end();
    });

//start a server on port 3000, any request that comes in should call
//the "app" function that is our middleware stack from above.
http.createServer(app).listen(3000);

Open up your terminal, cd to the directory you created in part 1, and run:

$ supervisor server.js

You can now open and http://localhost:3000 and updates to server.js will be visible each time you refresh.

As described previously, middleware can be added into a pipeline so that for each request that matches, various actions can be taken. The “connect” package we installed yesterday includes some useful middleware for us to get started:

var app = connect()
	//add logging to all requests
	.use(connect.logger())
	//do the original request handling added in part 1.
    .use(function(request, response){
        response.write('Hello from connect!');
        response.end();
    });

Now that we’ve added logging, save ‘server.js,’ if you’re running this using supervisor, you can refresh your application in your browser and see that the requests made to your server are now logged in your terminal.

The line “.use(connect.logger())” can seem a bit strange at first, but this is doing something that is very common in modern Javascript (and other functional languages), connect.logger() is a function that returns a… function.

If you remember the description of middleware from part 1, that function’s signature is:

function(request, response);

Depending on which languages you’re familiar with, this passing of functions can feel a bit strange. Trust me, this ends up being critical to successful javascript/node.js development, and a major strength of the language, as we will see later.

Another common scenario is to serve some static assets for things like css, client-side javascript, and (non-dynamic) html. A fairly standard convention in “rack-like” apps is that files located in a “public” folder in the root directory of your application will be served as static files. With the connect middleware, it’s very easy to add this functionality.

Let’s create a CSS, JS, and HTML file to illustrate this point.

Open a new instance of your terminal and cd to the base directory of your application.

$ mkdir public

Next, create a file called “index.html” and paste this content into it:

<!DOCTYPE html>
<html>
<head>
	<style>
		*{
			font-family: arial;
			padding: 0px;
			margin: 0px;
		}
		html{
			width: 100%;
			background: #cfcfcf;
		}
		body{
			text-align: center;
			width: 80%;
			margin: 20px auto;
			padding: 10px;
			background: white;
			border-radius: 5px;
			box-shadow: 0px 0px 5px 0px;
		}
	</style>
</head>
<body>
	Welcome to this static page!
</body>
</html>

Lastly, let’s add the connect middleware for static content to our stack in server.js

var app = connect()
	//add logging to all requests
	.use(connect.logger())
	//serve "static files from the public directory"
	.use(connect.static(__dirname + '/public'))
	//do the original request handling added in part 1.
    .use(function(request, response){
        response.write('Hello from connect!');
        response.end();
    });

Note that __dirname is “special” in node, and refers to the directory from which the node process was started. This is your application’s “root”, so we are able to append “/public” to the end of it so that we serve static files out of the public directory that we just created.

When you navigate to http://localhost:3000/, the index.html will be served (many http servers, including connect, will look for index.html and serve it if you don’t include a file name in the url).

When you navigate to another url on the site, note that the “static” middleware doesn’t kick in, and our middleware from the first part of this tutorial kicks in and handles the response (i.e. http://localhost:3000/part1). This is a very important property of middleware, pay close attention to this and think about what the implications are.

So far, I’ve left out one other important factor related to middleware, the concept of “next”, or “pass-through.”

You might have noticed that the logger middleware did not prevent the middleware that followed from running, and that the inclusion of the “static” middleware did prevent our base middleware from running, what’s going on?

To explain this, we need a brief digression into how javascript function parameters work…

Javascript functions can be defined with a certain number of parameters (this is called ‘Arity’), but then called with a different number of parameters, so this is totally legal:

var helloWorldMiddleware = function(request, response){
  if(request.url === '/hello_world'){
    response.write('Hello World!');
  }else{
    response.write('Goodbye Sweet World!');
  }
  response.end();
}

helloWorldMiddleware(obj1, obj2, obj3);

In this case, obj3 will be totally ignored by the function (though you can still get to it if you want via a special “arguments” variable). Again, this may seem peculiar at first, but is actually a very useful property of the language.

To keep things simple, we wrote a middleware with an Arity of two, but connect will actually call the function with three arguments:

  1. request (usually abbreviated ‘req’)
  2. response (usually abbreviated ‘res’)
  3. callback (usually called ‘next’, or ‘done’)

Notice that we have a new parameter “callback,” which can be used to pass control on to the next middleware if we do not wish to modify the response (as is the case with the logger middleware, and the static middleware when no static file matches the requested url path).

Let’s modify the helloWorldMiddleware I described above, allowing it to return ‘Hello World!’ when the path matches, but passing control to our original middleware when it does not:

//produce a special message when the url matches.
function(req, res, next){
	 	if(req.url === '/hello_world'){
	 		res.write('Hello World!');
	 		res.end();
	 	}else{
	 	  //pass control to the next middleware in the stack...
	 		next();
	 	}
};

If you’ve been following along, this is what your server.js file should have in it at this point:

//'require' says "pull in this installed package"
var connect = require('connect');

//we're still using Node's built in HTTP server, so let's pull it in.
var http = require('http');

//create our middleware stack (just send a basic message for now):
var app = connect()
	//add logging to all requests
	.use(connect.logger())
	 //serve "static files from the public directory"
	 .use(connect.static(__dirname + '/public'))
	 //produce a special message when the url matches.
	 .use(function(req, res, next){
	 	if(req.url === '/hello_world'){
	 		res.write('Hello World!');
	 		res.end();
	 	}else{
	 	  //pass control to the next middleware in the stack...
	 		next();
	 	}
	 })
	//do the original request handling added in part 1.
	.use(function(request, response){
		response.write('Hello from connect!');
		response.end();
	});

//start a server on port 3000, any request that comes in should call
//the "app" function that is our middleware stack from above.
http.createServer(app).listen(3000);

Navigating to http://localhost:3000/hello_world should produce

  Hello World!

While navigating to another path (like: http://localhost:3000/another_path) should produce:

  Hello from connect!

I’ll cover this in more detail in the next part of this tutorial, but this is very important, each middleware you create must either modify the request (usually by writing content to the response object), or call the callback (next()/done()). If your middleware doesn’t do this for every codepath, you will hang up your node server, and no new requests will be processed.

In the next part of the series, we’ll talk about the node process model, and start to dig into one of the central concepts in node.js, asynchronicity.