4. Writing Simple Applications

Now that you have a better understanding of how the JavaScript language really works, it’s time to start unleashing the power of Node.js to write web applications. As I mentioned in the introduction to this book, you will work on a small photo album website throughout this book. In this chapter you start by working on a JSON server to serve up the list of albums and pictures in each of those albums, and by the end, you add the capability to rename an album. In the process, you get a good understanding of the basics of running a JSON server, as well as how it interacts with a lot of the basic parts of HTTP such as GET and POST parameters, headers, requests, and responses. This first generation of the photo album application uses file APIs to do all its work; it’s a great way to learn about them and lets you focus on the new concepts you want to learn.

Your First JSON Server

At the end of Chapter 1, “Getting Started,” you wrote a little HTTP server that, for any incoming request, would return the plain text “Thanks for calling! ”. Now you can change this a little bit and have it do few things differently:

1. Indicate that the returned data is application/json instead of text/plain.

2. Print out the incoming request on console.log.

3. Return a JSON string.

Here is the trivial server, which is saved to simple_server.js:

var http = require('http');

function handle_incoming_request(req, res) {
    console.log("INCOMING REQUEST: " + req.method + " " + req.url);
    res.writeHead(200, { "Content-Type" : "application/json" });
    res.end(JSON.stringify( { error: null }) + " ");
}

var s = http.createServer(handle_incoming_request);
s.listen(8080);

Run this program in one terminal window (Mac/Linux) or command prompt (Windows) by typing

node simple_server.js

It should just sit there doing nothing, waiting for a request. Now, in another terminal window or command prompt, type

curl -X GET http://localhost:8080

If you’ve done everything correctly, in the window running the server, you should see

INCOMING REQUEST: GET /

In the window where you ran the curl command, you should see

{"error":null}

Try running different variations on the curl command and see what happens. For example:

curl -X GET http://localhost:8080/gobbledygook

Starting with this first new program, you can standardize the output of your JSON responses to always have an error field in the output. This way, calling applications can quickly determine the success or failure of the request. In cases in which a failure does occur, you always include a message field with more information, and for cases in which the JSON response is supposed to return some data, you always include a data field:

// failure responses will look like this:
{ error: "missing_data",
  message: "You must include a last name for the user" }

// success responses will usually have a "data" object
{ error: null,
  data: {
      user: {
          first_name: "Horatio",
          last_name: "Gadsplatt III",
          email: "[email protected]"
      }
  }
}

Some applications prefer to use numeric codes for their error systems. Using such codes is entirely up to you, but I prefer to use text ones because they feel more descriptive and save the step of looking things up when I use command-line programs like curl to test programs—I think no_such_user is far more informative than -325.

Returning Some Data

When you start, your photo album application is quite simple: it is a collection of albums, each of which contains a collection of photos, as shown in Figure 4.1.

Image

Figure 4.1 Albums and photos

For now, the albums are subfolders of the albums/ subfolder of the location where you run your scripts:

scripts/
scripts/albums/
scripts/albums/italy2012
scripts/albums/australia2010
scripts/albums/japan2010

So, to get your list of albums, you just need to find the items inside the albums/ subfolder. You do that using the fs.readdir function, which returns all items in the given folder except for “.” and “..”. The load_album_list function looks as follows:

function load_album_list(callback) {
    // we will just assume that any directory in our 'albums'
    // subfolder is an album.
    fs.readdir("albums", (err, files) => {
        if (err) {
            callback(err);
            return;
        }
        callback(null, files);
    });
}

Let’s walk through this function carefully. To start, it calls the fs.readdir function and provides a function that should be called when all the items in the directory have been loaded. This callback function has the same prototype that most callbacks have: an error parameter and a results parameter; you are free to call these whatever we want.

Note that the only parameter to the load_album_list function itself is a callback function. Because the load_album_list function is itself asynchronous, it needs to know where to pass the list of albums when it is finished with its work. It cannot return a value directly to the caller, because it will finish executing long before the fs.readdir function calls back to give you the results.

Again, this is the core technique of Node application programming: you tell Node to do something and where to send the results when it is done. In the meantime, you go on with other processing. Many of the tasks you have it perform basically end up being a long series of callbacks.

Listing 4.1 has the full code for the new album-listing server.

Listing 4.1 The Album-Listing Server (load_albums.js)


var http = require('http'),
    fs = require('fs');

function load_album_list(callback) {
    // we will just assume that any directory in our 'albums'
    // subfolder is an album.
    fs.readdir("albums", (err, files) => {
        if (err) {
            callback(err);
            return;
        }
        callback(null, files);
    });
}

function handle_incoming_request(req, res) {
    console.log("INCOMING REQUEST: " + req.method + " " + req.url);
    load_album_list((err, albums) => {
        if (err) {
            res.writeHead(500, {"Content-Type": "application/json"});
            res.end(JSON.stringify(err) + " ");
            return;
        }

        var out = { error: null, data: { albums: albums } };
        res.writeHead(200, {"Content-Type": "application/json"});
        res.end(JSON.stringify(out) + " ");
    });
}

var s = http.createServer(handle_incoming_request);
s.listen(8080);


In Listing 4.1, after fs.readdir has finished, you check the results. If an error occurs, you invoke the callback (the anonymous function you passed to load_album_list in the handle_incoming_request function) with the error object; otherwise, you send the list of folders (albums) back to the caller along with null indicating no error.

This listing adds some new error handling code to the handle_incoming_request function: if the fs.readdir function tells you that something bad has happened, you would like the caller to be made aware of that fact, so you still return some JSON, and the HTTP response code 503 to indicate that something unexpected has happened. The JSON servers should always return as much information as possible to their clients to help them determine if a problem is something they did or something internally wrong on the server itself.

To test the program, make sure the folder from which you are running this script has the albums/ subfolder with some album folders in it. To run the server, you again run

node load_albums.js

And to get the results, you use

curl -X GET http://localhost:8080/

The results from the curl command should look something like this:

{"error":null,"data":{"albums":["australia2010","italy2012","japan2010"]}}

Node Pattern: Asynchronous Loops

What happens if you create a text file called info.txt in your albums/ folder and rerun the album-listing server? You will probably see results like this:

{"error":null,"data":{"albums":["australia2010","info.txt","italy2012","japan2010"]}}

What you really want is for the program to check the results of fs.readdir and return only those entries that are folders, not regular files. To do this, you can use the fs.stat function, which passes an object you can use to test this.

So, rewrite the load_album_list function to loop through the results of fs.readdir and test whether they are folders:

function load_album_list(callback) {
    // we will just assume that any directory in our 'albums'
    // subfolder is an album.
    fs.readdir("albums", (err, files) => {
        if (err) {
            callback(err);
            return;
        }

        var only_dirs = [];

        for (var i = 0; files && i < files.length; i++) {
            fs.stat("albums/" + files[i], (err, stats) => {
                if (stats.isDirectory()) {
                    only_dirs.push(files[i]);
                }
            });
        }

        callback(null, only_dirs);
    });
}

Keep the rest of the program the same and then run the curl command. It should always return

{"error":null,"data":{"albums":[]}}

You broke the server! What happened?

The problem lies in the new for loop you added. Most loops and asynchronous callbacks are not compatible. Effectively, what you do in the preceding code is

Image Create an array only_dirs to hold the response.

Image For each item in the files array, call the nonblocking function fs.stat and pass it the provided function to test if the file is a directory.

Image After all these nonblocking functions have been started, exit the for loop and call the callback parameter. Because Node.js is single-threaded, none of the fs.stat functions will have had a chance to execute and call the callbacks yet, so only_dirs is still null, and you pass that to the provided callback. Indeed, when the callbacks to fs.stat are finally called, nobody cares any more.

To get around this problem, you have to use recursion. You effectively create a new function with the following format and then immediately call it:

var loop_iterator = (i) => {
  if( i < array.length ) {
     async_work( function(){
       loop_iterator( i + 1 )
     })
  } else {
    callback(results);
  }
}
loop_iterator(0);

Thus, to rewrite the loop testing whether or not the files result from fs.readdir are folders, you can write the function as follows:

function load_album_list(callback) {
    // we will just assume that any directory in our 'albums'
    // subfolder is an album.
    fs.readdir("albums", (err, files) => {
        if (err) {
            callback(err);
            return;
        }

        var only_dirs = [];

        var iterator = (index) => {
            if (index == files.length) {
                callback(null, only_dirs);
                return;
            }

            fs.stat("albums/" + files[index], (err, stats) => {
                if (err) {
                    callback(err);
                    return;
                }
                if (stats.isDirectory()) {
                    only_dirs.push(files[index]);
                }
                iterator(index + 1)
            });
        }
        iterator(0);
    });
}

Save this new version of the simple JSON server and then run the curl command, and you should now see the results with only album folders and no files included.

This recursive anonymous function works by not proceeding to the next item in the loop until the current item’s callback returns. When we get a value from fs.stat (i.e., our anonymous function is invoked indicating it has completed its work), only then do we call iterator(index + 1) to move to the next item. When there are no more items left, we finally invoke the callback function with the results. If there is an error along the way, we immediately call the callback and stop all further processing.

Learning to Juggle: Handling More Requests

The photo-album JSON server currently responds to only one kind of request: a request for a list of albums. Indeed, it doesn’t even really care how you call this request; it just returns the same thing all the time.

You can expand the functionality of the server a bit to allow you to request either of the following:

1. A list of albums available—you call this /albums.json

2. A list of items in an album—you can call this /albums/album_name.json

Adding the .json suffix to requests emphasizes that you are currently writing a JSON server that works only with that. There is no requirement for this; it is merely a convention I have adopted in all my servers.

A new version of the handle_incoming_request function with support for these two requests could be as follows:

function handle_incoming_request(req, res) {
    console.log("INCOMING REQUEST: " + req.method + " " + req.url);
    if (req.url == '/albums.json') {
        handle_list_albums(req, res);
    } else if (req.url.substr(0, 7) == '/albums'
               && req.url.substr(req.url.length - 5) == '.json') {
        handle_get_album(req, res);
    } else {
        send_failure(res, 404, invalid_resource());
    }
}

The two if statements in the preceding code are the bold ones; both look at the url property on the incoming request object. If the request is simply for /albums.json, you can handle the request as before. If it’s for /albums/something.json, you can assume it’s a request for the listing of an album’s contents and process it appropriately.

The code to generate and return the albums list has been moved into a new function called handle_list_albums, and the code to get an individual album’s contents is similarly organized into two functions called handle_get_album and load_album. Listing 4.2 contains the full listing for the server.

Starting with this new version of the code, we’ll change the output of the JSON server slightly: everything returned will be objects, not just arrays of strings. This helps you later in the book when you start generating UI to match the JSON responses. I italicized the code in Listing 4.2 that makes this change.

Although I try to avoid long, tedious, multipage code dumps later in this book, this first version of the server here is worth browsing through fully because most things you do after this are based on the foundation built here.

Listing 4.2 Handling Multiple Request Types


var http = require('http'),
    fs = require('fs');

function load_album_list(callback) {
    // we will just assume that any directory in our 'albums'
    // subfolder is an album.
    fs.readdir("albums", (err, files) => {
        if (err) {
            callback(make_error("file_error",  JSON.stringify(err)));
            return;
        }

        var only_dirs = [];

        var iterator = (index) => {
            if (index == files.length) {
                callback(null, only_dirs);
                return;
            }

            fs.stat("albums/" + files[index], (err, stats) => {
                if (err) {
                    callback(make_error("file_error",
                                        JSON.stringify(err)));
                    return;
                }
                if (stats.isDirectory()) {
                    var obj = { name: files[index] };
                    only_dirs.push(obj);
                }
                iterator(index + 1)
            });

        }
        iterator(0);
    });
}

function load_album(album_name, callback) {
    // we will assume that any directory in our 'albums'
    // subfolder is an album.
    fs.readdir("albums/" + album_name, (err, files) => {
        if (err) {
            if (err.code == "ENOENT") {
                callback(no_such_album());
            } else {
                callback(make_error("file_error",
                                    JSON.stringify(err)));
            }
            return;
        }

        var only_files = [];
        var path = `albums/${album_name}/`;

        var iterator = (index) => {
            if (index == files.length) {
                var obj = { short_name: album_name,
                            photos: only_files };
                callback(null, obj);
                return;
            }

            fs.stat(path + files[index], (err, stats) => {
                if (err) {
                    callback(make_error("file_error",
                                        JSON.stringify(err)));
                    return;
                }
                if (stats.isFile()) {
                    var obj = { filename: files[index],
                                desc: files[index] };
                    only_files.push(obj);
                }
                iterator(index + 1)
            });
        }
        iterator(0);
    });
}

function handle_incoming_request(req, res) {
    console.log("INCOMING REQUEST: " + req.method + " " + req.url);
    if (req.url == '/albums.json') {
        handle_list_albums(req, res);
    } else if (req.url.substr(0, 7) == '/albums'
               && req.url.substr(req.url.length - 5) == '.json') {
        handle_get_album(req, res);
    } else {
        send_failure(res, 404, invalid_resource());
    }
}

function handle_list_albums(req, res) {
    load_album_list( (err, albums) => {
        if (err) {
            send_failure(res, 500, err);
            return;
        }

        send_success(res, { albums: albums });
    });
}

function handle_get_album(req, res) {
    // format of request is /albums/album_name.json
    var album_name = req.url.substr(7, req.url.length - 12);
    load_album(album_name, (err, album_contents) => {
        if (err && err.error == "no_such_album") {
            send_failure(res, 404, err);
        }  else if (err) {
            send_failure(res, 500, err);
        } else {
            send_success(res, { album_data: album_contents });
        }
    });
}

function make_error(err, msg) {
    var e = new Error(msg);
    e.code = err;
    return e;
}

function send_success(res, data) {
    res.writeHead(200, {"Content-Type": "application/json"});
    var output = { error: null, data: data };
    res.end(JSON.stringify(output) + " ");
}

function send_failure(res, server_code, err) {
    var code = (err.code) ? err.code : err.name;
    res.writeHead(server_code, { "Content-Type" : "application/json" });
    res.end(JSON.stringify({ error: code, message: err.message }) + " ");
}


function invalid_resource() {
    return make_error("invalid_resource",
                      "the requested resource does not exist.");
}

function no_such_album() {
    return make_error("no_such_album",
                      "The specified album does not exist");
}

var s = http.createServer(handle_incoming_request);
s.listen(8080);


To avoid too much duplication of code, I also factored out a lot of the processing for sending the final success or response to the requesting client. This code is in the send_success and send_failure functions, both of which make sure to set the right HTTP response code and then return the correct JSON as appropriate.

You can see that the new function load_album is quite similar to the load_album_list function. It enumerates all the items in the album folder, then goes through each of them to make sure it is a regular file, and returns that final list. I also added a couple of extra lines of code to the error handling for fs.readdir in load_album:

if (err.code == "ENOENT") {
    callback(no_such_album());
} else {
    callback({ error: "file_error",
               message: JSON.stringify(err) });
}

Basically, if fs.readdir fails because it cannot find the album folder, that is a user error; the user specified an invalid album. You want to return an error indicating that fact, so you do that by using the helper function no_such_album. Most other failures, however, are likely to be server configuration problems, so you want to return the more generic "file_error" for those.

The output of getting the contents of /albums.json now looks as follows:

{"error":null,"data":{"albums":[{"name":"australia2010"},{"name":"italy2012"},{"name":
"japan2010"}]}}

After putting a few image files in each of the album folders, the output of getting the contents of an album (such as /albums/italy2012.json) now looks as follows (it’s been cleaned up here):

{
  "error": null,
  "data": {
    "album_data": {
      "short_name": "/italy2012",
      "photos": [
        {
          "filename": "picture_01.jpg",
          "desc": "picture_01.jpg"
        },
        {
          "filename": "picture_02.jpg",
          "desc": "picture_02.jpg"
        },
        {
          "filename": "picture_03.jpg",
          "desc": "picture_03.jpg"
        },
        {
          "filename": "picture_04.jpg",
          "desc": "picture_04.jpg"
        },
        {
          "filename": "picture_05.jpg",
          "desc": "picture_05.jpg"
        }
      ]
    }
  }
}

More on the Request and Response Objects

Now enter and run the following program:

var http = require('http');

function handle_incoming_request(req, res) {
    console.log("---------------------------------------------------");
    console.log(req);
    console.log("---------------------------------------------------");
    console.log(res);
    console.log("---------------------------------------------------");
    res.writeHead(200, { "Content-Type" : "application/json" });
    res.end(JSON.stringify( { error: null }) + " ");
}

var s = http.createServer(handle_incoming_request);
s.listen(8080);

Then, in another terminal window, run the curl command on something from this server:

curl -X GET http://localhost:8080

Your client window should just print error: null, but the server window prints an extremely large amount of text with information about the request and response objects passed to your HTTP server.

You’ve already used two properties on the request object: method and url. The former tells you if the incoming request is GET, POST, PUT, or DELETE (or something else such as HEAD), whereas the latter contains the URL requested on the server.

The request object is a ServerRequest object provided by the HTTP module included in Node.js, and you can learn all about it by consulting the Node documentation. You use these two properties and also see more in a little bit about handling POST data with the ServerRequest. You can also examine incoming headers by looking at the headers property.

If you look at the headers the curl program sends to you, you see

{ 'user-agent': 'curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r
zlib/1.2.5',
  host: 'localhost:8080',
  accept: '*/*' }

If you call the JSON server in the browser, you see something like

{ host: 'localhost:8080',
  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101
Firefox/16.0',
  accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'accept-language': 'en-US,en;q=0.5',
  'accept-encoding': 'gzip, deflate',
  connection: 'keep-alive' }

On the response side, you have already used two methods: writeHead and end. You must call end on the response object once and only once for each incoming request. Otherwise, the client never gets the response and continues to listen on the connection for more data.

When you are writing your responses, you should take care to make sure you think about your HTTP response codes (see the sidebar “HTTP Response Codes”). Part of writing your servers includes thinking logically about what you are trying to communicate to the calling clients and sending them as much information as possible to help them understand your response.

Increased Flexibility: GET Params

When you start adding a lot of photos to your albums, you will have too many photos to display efficiently on one “page” of the application, so you should add paging functionality to it. Clients should be able to say how many photos they want and what page they want, like this:

curl -X GET 'http://localhost:8080/albums/italy2012.json?page=1&page_size=20'

If you’re not familiar with the terminology, the bolded part of the preceding URL is the query string, commonly just referred to as the GET params for the request. If you run this curl command with the previous version of the program, you’ll probably notice that it doesn’t quite work anymore. If you add the following to the beginning of handle_incoming_request, you can see why:

console.log(req.url);

The URL now looks like this:

/albums/italy2012.json?page=1&page_size=20

The code is looking for the .json at the end of the string, not buried in the middle of it. To fix the code to handle paging, you have to do three things:

1. Modify the handle_incoming_request function to parse the URL properly.

2. Parse the query string and get the values for page and page_size.

3. Modify the load_album function to support these parameters.

You are fortunate in that you can do the first two in one fell swoop. If you add the url module that Node ships with, you can then use the url.parse function to extract both the core URL pathname and the query parameters. The url.parse function helps a little bit further in that you can add a second parameter, true, which instructs it to parse the query string and generate an object with the GET parameters in it. If you print out the results of url.parse on the preceding URL, you should see

{ search: '?page=1&page_size=20',
  query: { page: '1', page_size: '20' },
  pathname: '/albums/italy2012.json',
  path: '/albums/italy2012.json?page=1&page_size=20',
  href: '/albums/italy2012.json?page=1&page_size=20' }

Now you can modify the handle_incoming_request function to parse the URL and store it back on the request object in parsed_url. The function now looks like this:

var url = require('url');
function handle_incoming_request(req, res) {

    req.parsed_url = url.parse(req.url, true);
    var core_url = req.parsed_url.pathname;

    // test this fixed url to see what they're asking for
    if (core_url == '/albums.json') {
        handle_list_albums(req, res);
    } else if (core_url.substr(0, 7) == '/albums'
               && core_url.substr(core_url.length - 5) == '.json') {
        handle_get_album(req, res);
    } else {
        send_failure(res, 404, invalid_resource());
    }
}

For the last part, you modify the handle_get_album function to look for the page and page_num query parameters. You can set some reasonable default values for them when the incoming values are not provided or are not valid values (the servers should always assume that incoming values are dangerous or nonsensical and check them carefully).

function handle_get_album(req, res) {
    // get the GET params
    var getp = req.parsed_url.query;
    var page_num = getp.page ? parseInt(getp.page) : 0;
    var page_size = getp.page_size ? parseInt(getp.page_size) : 1000;

    if (isNaN(parseInt(page_num))) page_num = 0;
    if (isNaN(parseInt(page_size))) page_size = 1000;

    // format of request is /albums/album_name.json
    var core_url = req.parsed_url.pathname;

    var album_name = core_url.substr(7, core_url.length - 12);
    load_album(album_name, page_num, page_size, (err, album_contents) => {
        if (err && err.error == "no_such_album") {
            send_failure(res, 404, err);
        }  else if (err) {
            send_failure(res, 500, err);
        } else {
            send_success(res, { album_data: album_contents });
        }
    });
}

Note that we call parseInt in the above function since the query parameters are all strings by default.

Finally, you modify the load_album function to extract the subarray of the files_only array when it’s done with all its work:

function load_album(album_name, page, page_size, callback) {
    fs.readdir("albums/" + album_name, (err, files) => {
        if (err) {
            if (err.code == "ENOENT") {
                callback(no_such_album());
            } else {
                callback(make_error("file_error",
                                    JSON.stringify(err)));
            }
            return;
        }

        var only_files = [];
        var path = "albums/" + album_name + "/";

        var iterator = (index) => {
            if (index == files.length) {
                var ps;
                // slice fails gracefully if params are out of range
                var start = page * page_size
                ps = only_files.slice(start, start + page_size);
                var obj = { short_name: album_name,
                            photos: ps };
                callback(null, obj);
                return;
            }

            fs.stat(path + files[index], (err, stats) => {
                if (err) {
                    callback(make_error("file_error",
                                        JSON.stringify(err)));
                    return;
                }
                if (stats.isFile()) {
                    var obj = { filename: files[index], desc: files[index] };
                    only_files.push(obj);
                }
                iterator(index + 1)
            });
        }
        iterator(0);
    });
}

Modifying Things: POST Data

Now that I’ve largely covered how to get things from your JSON server, you might like to start being able to send data to it, either to create new things or modify existing ones. This is typically done with HTTP POST data, and you can send the data in many different formats. To send data using the curl client, you must do a few things:

1. Set the HTTP method parameter to POST (or PUT).

2. Set the Content-Type of the incoming data.

3. Send the data itself.

You can easily accomplish these tasks with curl. You do the first simply by changing the method name, the second by specifying HTTP headers with the -H flag to curl, and the last you can do in a few different ways, but here you can use the -d flag and just write the JSON as a string.

Now it’s time add some new functionality to the server to allow you to rename albums. Make the URL format as follows and specify that it must be a POST request:

http://localhost:8080/albums/albumname/rename.json

So, the curl command to rename an album is now the following:

curl -s -X POST -H "Content-Type: application/json"
     -d '{ "album_name" : "new album name" }'
     http://localhost:8080/albums/old_album_name/rename.json

Modifying handle_incoming_request to accept the new request type is pretty easy:

var url = require('url');
function handle_incoming_request(req, res) {

    // parse the query params into an object and get the path
    // without them. (2nd param true = parse the params).
    req.parsed_url = url.parse(req.url, true);
    var core_url = req.parsed_url.pathname;

    // test this fixed url to see what they're asking for
    if (core_url == '/albums.json' && req.method.toLowerCase() == 'get') {
        handle_list_albums(req, res);
    } else if (core_url.substr(core_url.length - 12)  == '/rename.json'
               && req.method.toLowerCase() == 'post') {
        handle_rename_album(req, res);
    } else if (core_url.substr(0, 7) == '/albums'
               && core_url.substr(core_url.length - 5) == '.json'
               && req.method.toLowerCase() == 'get') {
        handle_get_album(req, res);
    } else {
        send_failure(res, 404, invalid_resource());
    }
}

Note that you have to put the code to handle the rename request before the load-album request; otherwise, the code would have treated it as an album called rename and just executed handle_get_album.

Receiving JSON POST Data

To get the POST data in the program, you use a Node feature called streams. Streams are a powerful way to transfer large amounts of data in Node while maintaining the asynchronous, nonblocking nature of the system. I cover streams more fully in Chapter 6, “Expanding Your Web Server,” but for now, you just need to know the key pattern for using streams:

.on(event_name, function (parm) { ... });

In particular, pay attention to two events for now: the readable and end events. The stream is actually just the ServerRequest object from the http module (which inherits from the class Stream; ServerResponse does too!), and you listen to these two events via the following pattern:

    var json_body = '';
    req.on('readable', () => {
        var d = req.read();
        if (d) {
            if (typeof d == 'string') {
                json_body += d;
            } else if (typeof d == 'object' && d instanceof Buffer) {
                json_body += d.toString('utf8');
            }
         }
    });

    req.on('end', () => {
        // did we get a valid body?
        if (json_body) {
            try {
                var body = JSON.parse(json_body);
                // use it and then call the callback!
                callback(null, ...);
            } catch (e) {
                callback({ error: "invalid_json",
                           message: "The body is not valid JSON" });
        } else {
            callback({ error: "no_body",
                       message: "We did not receive any JSON" });
            }
        }
    });

For each piece (chunk) of data forming the body of the incoming request, the function you pass to the on('readable', ...) handler is called. In the preceding code, you first read the data from the stream with the read method and append this incoming data to the end of the json_body variable; then when you get the end event, you take the resulting string and try to parse it. JSON.parse throws an error if the given string is not valid JSON, so you have to wrap it in a try/catch block.

The function to process the request for a rename is as follows:

function handle_rename_album(req, res) {

    // 1. Get the album name from the URL
    var core_url = req.parsed_url.pathname;
    var parts = core_url.split('/');
    if (parts.length != 4) {
        send_failure(res, 404, invalid_resource());
        return;
    }

    var album_name = parts[2];

    // 2. Get the POST data for the request. This will have the JSON
    //    for the new name for the album.
    var json_body = '';
    req.on('readable', () => {
        var d = req.read();
        if (d) {
            if (typeof d == 'string') {
                json_body += d;
            } else if (typeof d == 'object' && d instanceof Buffer) {
                json_body += d.toString('utf8');
            }
        }
    });

    // 3. When we have all the post data, make sure we have valid
    //    data and then try to do the rename.
    req.on('end', () => {
        // did we get a valid body?
        if (json_body) {
            try {
                var album_data = JSON.parse(json_body);
                if (!album_data.album_name) {
                    send_failure(res, 404, missing_data('album_name'));
                    return;
                }
            } catch (e) {
                // got a body, but not valid json
                send_failure(res, 403, bad_json());
                return;
            }

            // we have a proposed new album name!
            do_rename(album_name, album_data.album_name, (err, results) => {
                if (err && err.code == "ENOENT") {
                    send_failure(res, 403, no_such_album());
                    return;
                } else if (err) {
                    send_failure(res, 500, file_error(err));
                    return;
                }
                send_success(res, null);
            });
        } else {
            send_failure(res, 403, bad_json());
            res.end();
        }
    });
}

Notice that the 'readable' event does not actually pass in the object from which to read—it merely specifies that the req object has some data to read, and we thus read from that.

The complete listing for the updated server that can now handle three requests is in the Chapter 4 GitHub source code as post_data.js.

Receiving Form POST Data

Although you won’t use this as much in your application, a lot of data sent to servers from web applications is sent via <form> elements, for example:

  <form name='simple' method='post' action='http://localhost:8080'>
    Name: <input name='name' type='text' size='10'/><br/>
    Age: <input name='age' type='text' size='5'/><br/>
    <input type='submit' value="Send"/>
  </form>

If you write a little server program to fetch the POST data using the readable and end events as you did in the preceding section, printing out the data for the preceding form yields:

name=marky+mark&age=23

What you really need, however, is something similar to what you got previously with JSON: a JavaScript object that represents the data sent to you. To achieve this, you can use another module built into Node.js, querystring, and specifically, its parse function, as follows:

var POST_data = qs.parse(body);

The resulting object is as you expect it to be:

{ name: 'marky mark++', age: '23' }

The complete listing of the simple server to receive a form and print out its contents is as follows:

var http = require('http'), qs = require('querystring');

function handle_incoming_request(req, res) {
    var body = '';
    req.on('readable', () => {
        var d = req.read();
        if (d) {
            if (typeof d == 'string') {
                body += d;
            } else if (typeof d == 'object' && d instanceof Buffer) {
                body += d.toString('utf8');
            }
        }
    });

    // 3. when we have all the post data, make sure we have valid
    //    data and then try to do the rename.
    req.on('end', () => {
        if (req.method.toLowerCase() == 'post') {
            var POST_data = qs.parse(body);
            console.log(POST_data);
        }
        res.writeHead(200, { "Content-Type" : "application/json" });
        res.end(JSON.stringify( { error: null }) + " ");
    });
}

var s = http.createServer(handle_incoming_request);
s.listen(8080);

Starting in Chapter 6, however, when you learn how to use the Express web application framework for Node, you’ll see that this functionality is typically all handled for you.

Summary

This chapter covered quite a lot of new material. You wrote your first web application by writing a simple JSON server. I like this approach for a few key reasons:

Image It lets you focus on the server and the key concepts you need to get really comfortable with Node.js.

Image You can make sure your server API is well organized and efficient.

Image A nice, light application server should mean that the computer(s) running it will be able to handle the load without too much trouble. When you add in HTML UI and client-side features later, you can try to have the client do as much of the work as possible.

Now you not only have a basic working server but also have seen how to modify the output it sends you with different request URLs and query parameters. You have also seen how to send new data or modify existing data by submitting POST data along with the request. Although some of the programs look reasonably long and complicated already, you are working with just basic components right now to examine core Node principles. You start to replace these with more helpful and functional modules quite soon.

But first, take a break and learn more about modules—much more on how to consume them and also how to write your own.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset