Building A

Simple Node.js HTTP server


The Code

Here I am going to explain briefly how the whole thing works. Please rememer though, this is NOT my own code, it's best example I could find out there, it's very simple, neat and easy to understand.

The code blocks with syntax highlighting is courtesy of highlight.js which I found very simple to use and import into this project. It comes with a variety of themes, I used the base16/darkula theme for this project.

Modules used

The code is using the http module to do most the work here. The path module is used to help resolve filenames etc and the fs module will allow us to load our html etc to send to the client. These are loaded first and assigned constants:


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

http module code

Now we can use the http module's createServer function, we start like this:

const server = http.createServer((req, res) => {

This takes a function as an argument with two parameters. The first parameter is req which is the request from the client, this will allow us to access the URL and any query strings sent to the server. The second argument is res which is the response. I think of this as my "handle" to the functions I can use to send a response back to the client.

Now we have the function (an arrow function in this case). This is called once the request has been recieved (it's a callback function). Inside here we can make an appropriate response to the client's request.

Parse the URL

Now we need to see what URL the client sent us and parse it so that we can use it to work out what file we will send:


        // Build file path
        let filePath = path.join(
          __dirname,
          "public",
          req.url === "/" ? "index.html" : req.url
        );
      
        // Extension of file
        let extname = path.extname(filePath);
    

Now the path module is used. The first function used is path.join Noto how we are using the constant we assigned when we imported the module using "require". Join is pretty self explanatory, it's concatenation with some extras built in that help ensure the path built is correct for the local file system etc.

The __dirname is basically an environment variable (think global variable set by node, roughly speaking..) which contains the working directory for the current file we are currently executing, index.js in our case. the "public" string happens to be the name of the directory on the server that the files sent to the client are going to be stored within (think HTDOCS on 'apache' etc). Finally, req.url uses the "req" parameter we discussed earlier and allows us to extract the url from the request. For example, if it's "contact.html", that's what will be returned. With all this combined, we have a complete file path that can be used later to retrieve files from the "public" directory.

The path.extname simply returns the extension of the filename at the end of the url. This will be used next to set the media type like so:


        // Initial content type
        let contentType = "text/html";

        switch (extname) {
            case ".js":
              contentType = "text/javascript";
              break;
            case ".css":
              contentType = "text/css";
              break;
            case ".json":
              contentType = "application/json";
              break;
            case ".png":
              contentType = "image/png";
              break;
            case ".jpg":
              contentType = "image/jpg";
              break;
          }

          // Check if contentType is text/html but no .html file extension
          if (contentType == "text/html" && extname == "") filePath += ".html";
        
    

Basically, if the client browser is to be expected to process the information sent to it, it will need to know the type of data it is sent. Not much point trying to display an image as text, etc. The extension extracted earlier is now used as a condition for the switch block and the contentType string can be set. Note that during declaration it is set a default value of "text/html", further to this, after the switch block, anything without an extension is assigned the 'html' extension. I'm not sure why it's checking for the 'contentType' here, that's redundant because anything that bypassed the switch block IS that type and with no extension that is precisely what will happen. In the video, this code is built in stages with re-factoring so I think it's just a remnant from an earlier version.

The file handling

Next, the callback function from createServer, needs to send a response back to fulfil our request otherwise the user of the client machine will have nothing to look at. This is when the 'fs module comes in, allowing us to interact with the filesystem. Here's the code:


        // Read File
        fs.readFile(filePath, (err, content) => {
          if (err) {
            if (err.code == "ENOENT") {
              // Page not found
              fs.readFile(
                path.join(__dirname, "public", "404.html"),
                (err, content) => {
                  res.writeHead(404, { "Content-Type": "text/html" });
                  res.end(content, "utf8");
                }
              );
            } else {
              //  Some server error
              res.writeHead(500);
              res.end(`Server Error: ${err.code}`);
            }
          } else {
            // Success
            res.writeHead(200, { "Content-Type": contentType });
            res.end(content, "utf8");
          }
        });
      });
    

The filePath variable creted earlier using the path module gives us the path and filename we need to access. This is done with the fs.readFile function. This takes a filepath as the first parameter and another callback function for the second which will be executed upon completion.

The contents of the file will be loaded into the content parameter (declared 'on the fly'). The err parameter (also declared 'on the fly') will hold any error thrown by the readFile function. Within the callback function, the err variable can now be used to detect if there was an error. Whatever the outcome, it can be evaluated and responded to here.

The first if statement checks for an error. If all is OK, it skips all the way down to the second else (commented as success) and uses the res.writeHead function to write the outgoing header code 200 (SUCCESS) and the content type that was set earlier to the client. The res.end sends the content variable, thus the data from the file to the client before closing the response. Note the "utf8" string as the second parameter which tells the client the output type of the data being sent (not to be confused with content type).

What if something went wrong? going back up the code to our first if statement, there is another 'if' block looking for an error equal to the string ENOENT. This is an abbreviation of "Error NO ENTry which dates back to the days of C compilers when they only allowed variable names, etc up to 8 characters long (a good comparison is the old FAT8 8.3 filenames). It's stuck around and now is used as the error code for "File not found". Using basically the same code we had for a successful load, it sends a "404" code in the header and then loads a custom error page. I quite like mine, feel free to put in an invalid url and test it :-)

The final part of the error handling is to account for any other type of error. In this case, it just defaults to a "500" code in the header and then outputs "Server error: " followed by the error code to the client.

Now we must listen

Precisely that. The server will not be active and listening for client requests unless it has been told to do so AND on which port it should listen to. The last two lines of code sort this:


        const PORT = process.env.PORT || 5000;

        server.listen(PORT, () => console.log(`Server running on port ${PORT}`));
    

The process.env.PORT environment variable on the server in a "real world application" should signify the correct free port to assign the listener to. If that is not set (just as it isn't within my test environment), the logical OR (||) operator will assign a default value of 5000. To finish up, the server constant can be used to invoke the server.listen function. This accepts the parameters of the port number (that the "PORT" constant can now be used for) and a callback function. In this instance, the callback is just used to log the port number for the console which is useful in development to verify it's working. This call back could be used for anything else you wanted to initialise or setup immediately after the listener has been set. You do not have to use this parameter. There are other optional parameters like "hostname" and "backlog" not used here. No doubt I'll learn them in time.

Possible improvements

Firstly, in it'sintended purpose, I feel there is little to improve. the code did an excellent job of helping me to get a start on node.js. Any potential improvement serves this purpose of helping me learn too. To start with, there is the obvious ability to introduce another custom error page in replacement of a message for the "500 server error" http code. This would only need a couple of lines changed and another custom html page to be gerenated. A briefly tweaked copy of the existing 404 page would work just fine. Whilst in this part of the code, there may be considerations for other codes being handled? 403 is a commonly used page, that would be more practical in use if we were using an authentication system, which we're not at present. Finally, if we do expand this section, maybe the use of a switch block could clean up the code somewhat.

At my current level of ability, I don't really have anything else to add. I've enjoyed exploring and learning about this, it has given me a good foundation to progress forwards, perhaps next with express.js I think and learn about using it to make a REST API.