Nodejs Book: Chapter 4

In the last chapter we finished a simple file server with similar basic functionality
to Apache. That being said though, we have a long function to execute a very specific
task. It would be ideal if we could create a module to separate that function to
make our clean cleaner and shorter. The good news is that it’s entirely possible.
If you recall, we already installed two libraries into our project, “mime” and
“async”, and the better news is that we can create our own libraries and modules
to be installed in the same way.

We can copy and paste our function into it’s own source file for handling files.

File: handle-file.js

"use strict";

const fs = require("fs");
const mime = require("mime");
const async = require("async");

const PATH = "public";

module.exports = function (req, res) {

    fs.stat(PATH + req.url, function( err, stats ) {
        if( err ) {
            res.writeHead(404, {"Content-Type" : "text/plain"});
            return res.end("File Not Found");
        }

        if( stats.isFile() ) {

            res.writeHead(200, {"Content-Type" : mime.lookup(req.url) });
            let stream = fs.createReadStream(PATH + req.url);
            stream.pipe(res);

        } else if( stats.isDirectory() ) {

            if(req.url[req.url.length - 1] !== "/") {

                res.writeHead(302, { "Location" : req.url + "/" });
                return res.end();

            }

            fs.readdir(PATH + req.url, function(err, files) {
                if(err) {
                    throw err;
                }

                if(files.indexOf("index.html") !== -1) {

                    res.writeHead(200, {"Content-Type" : "text/html" });
                    let stream = fs.createReadStream(PATH + req.url + "index.html");
                    stream.pipe(res);
                    return;

                }

                let file_list = [];
                let dir_list = [];

                async.eachSeries(files, function(file, nextFile) {

                    fs.stat(PATH + req.url + file, function( err, stats ) {
                        if(err) {
                            throw err;
                        }

                        if(stats.isFile()) {

                            file_list.push(file);

                        } else if(stats.isDirectory()) {

                            if(file !== ".") {
                                dir_list.push(file);
                            }

                        }

                        nextFile();
                    });

                }, function() {

                    file_list.sort();
                    dir_list.sort();

                    res.writeHead(200, {"Content-Type" : "text/html" });
                    res.write("<!DOCTYPE HTML>");
                    res.write("<html>");
                    res.write("<head>");
                    res.write("<meta charset=\"utf-8\">");
                    res.write("<title>Index</title>");
                    res.write("</head>");
                    res.write("<body>");
                    res.write("<h1>"+req.url+"</h1>");
                    res.write("<ul>");

                    dir_list.forEach(function(dir){

                        res.write("<li>");
                        res.write("<a href=\"" + dir + "/\">");
                        res.write(dir + "/");
                        res.write("</a>");
                        res.write("</li>");

                    });

                    file_list.forEach(function(file){

                        res.write("<li>");
                        res.write("<a href=\"" + file + "\">");
                        res.write(file);
                        res.write("</a>");
                        res.write("</li>");

                    });

                    res.write("</ul>");
                    res.write("</body>");
                    res.end("</html>");

                });

            });

        } else {

            res.writeHead(404, {"Content-Type" : "text/plain"});
            return res.end("File Not Found");

        }

    });

}

Next we can run “`$ npm init“` in the directory we created our source file in
to create a package.json file describing the source file we created for Node to
include in other files. We will be asked to answer a few answers about our package
but the result should look similar to the following.

File: package.json

{
  "name": "handle-file",
  "version": "1.0.0",
  "description": "A simple function for serving static files from a \"public\" folder",
  "main": "handle-file.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "handle",
    "file",
    "server",
    "http"
  ],
  "author": "Benjamin Collins",
  "license": "MIT"
}

But there’s one thing missing. Since we’re using the libraries “mime” and “async”
, we need to include that information into our package.json file. That will be
done automatically is we run the command:

$ npm install async mime --save

After doing that, our package.json should shoud now have a “dependencies” attribute.

"dependencies": {
  "async": "^2.3.0",
  "mime": "^1.3.4"
}

Now we’re almost ready to publish. Before doing so, we should create a “readme.md”
explaining our code and what it does so other people can use it. Plus it’s always
helpful if we forget. If you don’t already, you can create an account on
https://www.npmjs.com. Once you’ve done so, you can login via the command line
with:

$ npm adduser
Username: [ username-here ]
Password: [ password-here ]
Email: (this IS public) your-email@domain.com
Logged in as username-here on https://registry.npmjs.org/.

Then we can publish our created module with:

$ npm publish .

We can now include our function to handle files like we did with “mime” and “async”
with:

$ npm install file-handle

And our final code for this chapter is much shorter, looks like the following:

File: file_server.js

"use strict";

const http = require("http");
const handleFile = require("handle-file");

const server = http.createServer();
server.on("request", handleRequest);
server.listen(8080, handleListen);

function handleRequest(req, res) {

    handleFile(req, res);

}

function handleListen() {

    console.log("Server is listening on port 8080");

}

And with that we’ve learned how to make modules and cleaned up our code. With a
basic file server and cleaner code we are going to focus more on interaction with
client web browsers in the next chapter.