Nodejs Book: Chapter 2

In our previous chapter we made a simple server that returned the text “Hello, World!” for any given request. So how do we change modify our code to perform something more useful? If we look at a server like Apache, each URL represents a file path on the server. If the file exist, Apache returns the file, otherwise it returns and error. Let’s try to replicate this behavior with Nodejs.

But before we do so, we need to install a library to help us. Run the following command in your project directory.

$ npm install mime

More on what this library does and why we need it later in this chapter. Next we need a directory to serve static files from. So let’s make one.

$ mkdir public

Next is our code for a static web server.

// File: status.js
"use strict";

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

const PATH = "public";

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

function handle_request(req, res) {

    req.url = PATH + req.url;

    fs.stat(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(req.url);
            stream.pipe(res);

            stream.on("end", function() {
                console.log("Sent: %s", req.url);
            });

        } else {

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

        }

    });

}

function handleListen( ){
    console.log("Server is listening on port 8080");
}

But it’s useless to run this server without any files in our public folder,
so let’s make two quick files for testing.

<!-- File: public/index.html -->
<!DOCTYPE HTML>
<html>

    <head>

        <title>Pages</title>
        <meta charset="UTF-8"/>

        <link rel="stylesheet" type="text/css" href="layout.css"/>

    </head>

    <body>

        <h1>Hello, World!</h1>

    </body>

</html>

And then we also need to define the CSS file, to make sure we can get multiple files.

// File: public/layout.css

h1 {

    color: blue;

}

We create a simple index.html file and stylesheet which is referenced inside the html file. Now we can run our Nodejs server with:

$ node static.js

And if we open the url http:://192.168.1.16:8080/index.html in our browser we should see the following.

And if we try a random url, we should see a file not found error.

Now that we have a working server, let’s take another look at our source code to see what’s going on.

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

In terms of imports, we have two more than the http module we used last time. In addition we also have fs for reading and writing files to the file system. We also have the mime type that we imported from the node package manager.

A “MIME” is a media identifier type and at first glance it’s not inherently obvious as to why it’s required. When information is sent to and from a server is broken down into numbers, and more specifically zero’s and one’s. But let’s stick with numbers. So for example if we were to send the text “hello”, from one computer to another, the ASCII code for these letters would be <104> <101> <108> <108> <111>. So our client would get these numbers, but how is the client supposed to interpret these numbers? Are they just numbers? Are they characters? Could they color values? To avoid confusion we send a header ahead from the server to the client saying, the content type of the information being sent is plain text.

There are a lot of mime types to keep track of. So rather than trying to maintain a list of all of them, it’s better to simply use an existing library if available. More example Mime types are listed below.

audio/mpeg
audio/vorbis
multipart/form-data
text/css
text/html
text/plain
image/png
image/jpeg
image/gif

Next we can look at our updated handleRequest function.

function handle_request(req, res) {

    req.url = PATH + req.url;

    ...

}

The first thing that sticks out if the req.url = PATH + req.url; line.
If we take another look at our URL we can see where this is coming from. Our full request was http://192.168.1.16:8080/index.html. The “http://” on the front is the protocol being used. Protocol is not a word that comes up in common human language, but it’s a little more understandable when looking at a different alternative. Http standard for “Hyper Text Transfer Protocol”. We can contrast this with something like FTP or “File Transfer Protocol”. So the client and servers are saying, “I would like to communicate with text messages”. The next piece is “192.168.1.16”, this is the Internet Protocol Address. It’s the address of where the server is located on the network. The “:8080” is the port, kind of like the apartment number. Even if we have the address right, there might not be anyone home for a given room. So our application is listening on port 8080.

This means that the front part of the URL http://192.168.1.16:8080 is just telling the request where to go. Only the /index.html gets passed into our application as the request. So what’s index.html? It’s a file. It’s a file, where? This is up to us to decide, and we made a folder on our system to host files to be served to other computers, so we add “public” onto the front for “public/index.html” to try and read the file. So our file check logic is the following.

fs.stat(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(req.url);
        stream.pipe(res);

        stream.on("end", function() {
            console.log("Sent: %s", req.url);
        });

    } else {

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

    }

});

We use fs.stat to see if the file exists in the fist place. If the file
doesn’t exist, the function will return an error to which we return a 404 file not found error. If the file is found and exists, then we send the client a status of 200 meaning that yes the file does exist, and after that we have some funny looking code.

This funny code is opening a file stream. It would be simpler to read the entire file in Nodejs and return it to the client, but this would use more memory than required. So the information read from the file system is redirected directly to the response.

Lastly we have another 404 at the end in case the path found is not a file. It could be a directory, or socket, symbolic link and otherwise. Right now we just want to serve static files, so we simply return an error.

If you were wondering where these numbers 200, and 404 are coming from. These are http status codes. These are codes that are sent from the server to the client to indicate different statuses. 1xx codes for information, 2xx codes are for success, 3xx for redirects, 4xx for client errors and 5xx for server errors. These are several standard codes that all browsers understand. But it’s also possible to create your own custom status codes, such as Cloud Flare’s 520 “Unknown Error”, but make sure your custom codes don’t overlap with standards, and other popular unofficial codes before doing so.

So we created a very simple static server. But don’t file servers like Apache
and Nginx give directory listings, and server index.html files automatically? The answer is yes they do, and we will continue to improve our web server in the next chapter where we implement this functionality.