Callum Kerr

“There is no place I know,
like the world of pure imagination.”



Email me  About me  


< All Posts

Building a Node Server (Session 2)

Dynamically Loading Events

At the end of our first session, we had a working server, rendering a Jade template and injecting a an array of event objects for the upcoming events list. We want to pull out this list of events, and get the server to pull from some external data source, so that the web site will remain up to date, without any of us having to go in there and update it manually. Here's where we started:

/*  
        This is a simple node server that will respond to requests on localhost.
        Call it from the command line with `node serve.js [port]`
    
        This script is intended for learning purposes. 
        A production system would require much more robust error handling.
    
        However, it serves to help us understand how node interoperates with http requests
        and file system resources.
    
        It will serve html, js and css files directly, and will render jade files to html.
        This naive implementation relies on the browser to parse the content-type of the 
        requested files ("text/css", "text/html", "image/jpeg", etc.).
    */
    
    // Import node packages with the `require` function.
    // This provides utility functions for http and filesystem resources
    var http = require("http"),
      url = require("url"),
      path = require("path"),
      fs = require("fs"),
      // template engine: http://jade-lang.com
      jade = require('jade'),
    
      // Port can be set from the command line, or default to 8888
      port = process.argv[2] || 8888;
    
    // Start the server. 
    // The createServer() function takes a callback (receiving request and response objects)
    // that will be executed on each request.
    http.createServer(function(request, response) {
    
      // parse the request url to find the file that is being requested.
      var uri = url.parse(request.url).pathname,
        // the filename is built from the cwd (current working directory) of 
        // the current process, and the parsed uri path
        filename = path.join(process.cwd(), uri);
    
      // If the requested filename is a directory (eg, '/', the site root),
      // we will render and serve the 'index.jade' file from that directory
      if (fs.statSync(filename).isDirectory()) filename += '/index.jade';
    
      // fs.exists checks if the file exists
      // It will be deprecated and should be replaced with a call to fs.open
      // (http://nodejs.org/api/fs.html#fs_fs_exists_path_callback)
      fs.exists(filename, function(exists) {
        // however, in this case it makes it easy for us to return a 404 if the file isn't found
        if (!exists) {
          response.writeHead(404, {
            "Content-Type": "text/plain"
          });
          response.write("404 Not Found\n");
          response.end();
          return;
        }
    
        // Read the file in binary format. The callback receives any errors, and the file
        fs.readFile(filename, "binary", function(err, file) {
          // Respond to errors with Status code 500: "Internal server error"
          if (err) {
            response.writeHead(500, {
              "Content-Type": "text/plain"
            });
            response.write(err + "\n");
            response.end();
            return;
          }
    
          // Render the file through jade if file extension is .jade
          if (path.extname(filename) === '.jade') {
            // the `locals` var is passed to the template engine, for dynamic data
            // TODO: replace this with a calendar service
            var locals = {
              events: [{
                time: "Tuesday, February 24th: 6pm - 8pm",
                description: "NodeSchool Meetup - Hot Pink, Ink"
              }, {
                time: "Tuesday, March 10th: 6pm - 8pm",
                description: "HTML Preprocessor Workshop - Hot Pink, Ink"
              }]
            }
            // render the file
            file = jade.render(file, locals)
          }
          // And respond. Success!
          response.writeHead(200);
          response.write(file, "binary");
          response.end();
        });
      });
    // The server won't start listening until we get to this part.
    }).listen(parseInt(port, 10));
    
    // Write to the console, to let the user know that it's runninng.
    console.log("Static file server running at\n  => http://localhost:" + port + "/\nCTRL + C to shutdown");
    

First, we had a few issues to iron out. First, the server would crash any time it encountered a request for a file that didn't exist on the local file-system. And I found that Chrome sends a request for favicon.png in the domain root automatically, which doesn't exist.

This occurs because of the call to fs.statSync(filename) (to load the index file of a directory) outside the call to fs.exists

In addition we were using a call to the deprecated function fs.exists, so we fixed that, and you can view the refactored code below, or see the changes on github.

/*
        This is a simple node server that will respond to requests on localhost.
        Call it from the command line with `node serve.js [port]`
    
        This script is intended for learning purposes.
        A production system would require much more robust error handling.
    
        However, it serves to help us understand how node interoperates with http requests
        and file system resources.
    
        It will serve html, js and css files directly, and will render jade files to html.
        This naive implementation relies on the browser to parse the content-type of the
        requested files ("text/css", "text/html", "image/jpeg", etc.).
    */
    
    // Import node packages with the `require` function.
    // This provides utility functions for http and filesystem resources
    var http = require("http"),
      url = require("url"),
      path = require("path"),
      fs = require("fs"),
      // template engine: http://jade-lang.com
      jade = require('jade'),
    
      // Port can be set from the command line, or default to 8888
      port = process.argv[2] || 8888;
    
    // Start the server.
    // The createServer() function takes a callback (receiving request and response objects)
    // that will be executed on each request.
    http.createServer(function(request, response) {
    
      // parse the request url to find the file that is being requested.
      var uri = url.parse(request.url).pathname,
        // the filename is built from the cwd (current working directory) of
        // the current process, and the parsed uri path
        filename = path.join(process.cwd(), uri);
    
      try {
        // If the requested filename is a directory (eg, '/', the site root),
        // we will render and serve the 'index.jade' file from that directory
        if (fs.statSync(filename).isDirectory()) filename += '/index.jade';
      } catch (error) {
        if (error.code === 'ENOENT') {
          // This is a File not found error: return 404
          response.writeHead(404, {
            "Content-Type": "text/plain"
          });
          response.write("404 Not Found\n");
          response.end();
          return;
    
        } else {
          // Respond to other errors with Status code 500: "Internal server error"
          response.writeHead(500, {
            "Content-Type": "text/plain"
          });
          response.write(err + "\n");
          response.end();
          return;
        }
      }
    
      // Read the file in binary format. The callback receives any errors, and the file
      fs.readFile(filename, "binary", function(err, file) {
        if (error && error.code === 'ENOENT') {
          response.writeHead(404, {
            "Content-Type": "text/plain"
          });
          response.write("404 Not Found\n");
          response.end();
          return;
        } else if (error) {
          // Respond to other errors with Status code 500: "Internal server error"
          response.writeHead(500, {
            "Content-Type": "text/plain"
          });
          response.write(err + "\n");
          response.end();
          return;
        }
    
        // Render the file through jade if file extension is .jade
        if (path.extname(filename) === '.jade') {
          // the `locals` var is passed to the template engine, for dynamic data
          // TODO: replace this with a calendar service
          var locals = {
            events: getCalendarEvents()
          }
          // render the file
          file = jade.render(file, locals)
        }
        // And respond. Success!
        response.writeHead(200);
        response.write(file, "binary");
        response.end();
      });
    
    // Tell the server to start listening on the designated port.
    }).listen(parseInt(port, 10));
    
    // Write to the console, to let the user know that it's runninng.
    console.log("Static file server running at\n  => http://localhost:" + port + "/\nCTRL + C to shutdown");
    

In doing this, I accidentally removed the events object, in anticipation of refactoring to a function call, and so I added it back in via a function call here. (Just in case you were wondering).

We were now ready to perform some api calls to get our events data. Our first attempt was to use facebook's open graph to get an event listing from the public group. We did eventually have some success in calling to the facebook servers. I always get a thrill from talking to some thrid-party API, when it finally clicks, and the server returns a meaningful message. Unfortunately, in this case, the message we received from Facebook was that we would be unable to get the events listing using an app token, we would need a User Access Token, which would require us to have all our site visitors authenticate with Facebook to generate the needed token, or to perform some kind of hackery by encoding a user_access token from one of our own accounts (or create a new account for this purpose) and figure out how to regenerate it every 60 days. And that feels a little close to skirting facebook's permnissions policy, but it is perhaps a viable option.

With the end of our session, we turned to the google calendar API, as an alternative data source for events listing. We ran out of time before making any real progress here, and so we agreed to look at the doucmentation over the interval, and try again next session.

< Part 1 Part 3 >