Serving Static files with Node JS

Posted in software by Christopher R. Wirz on Mon Feb 13 2017

Node JS offers an advantage over other server types because you can elect to keep certain contents in the cache (in memory). This allows for faster transactions of static data as there are fewer read write operations to disk or database. For text and serialized content, this offers a huge advantage. Some may also use this feature for small often-requested images as well. Overloading the cache will slow down the Node JS instance, but putting the right content in the cache will make for a great user experience.

Why not have a balance? The developer should specific the assets to be loaded in the cache while hosting the static files normally. This hybrid approach creates a seamless user experience and does not risk over-tasking the server.

The following server.js file provides examples of loading files (theme.html) into cache while serving other files statically. Because so many of my colleagues use services like openshift, I included checks for environment variables in this example.


// server.js
#!/bin/env node
var express = require('express'), http = require('http');
var fs      = require('fs');
var path = require('path');

/**
 *  Define the sample application.
 */
var App = function() {

    //  Scope.
    var self = this;
    self.staticBasePath = "./public";

    /**
     *  Set up server IP address and port # using env variables/defaults.
     */
    self.setupVariables = function() {
        //  Set the environment variables we need.
        self.ipaddress = process.env.OPENSHIFT_NODEJS_IP;
        self.port      = process.env.OPENSHIFT_NODEJS_PORT || 8080;

        if (typeof(self.ipaddress) == "undefined") {
            //  Log errors on OpenShift but continue w/ 127.0.0.1 - this
            //  allows us to run/test the app locally.
            console.warn('No OPENSHIFT_NODEJS_IP var, using 127.0.0.1');
            self.ipaddress = "127.0.0.1";
        }
    };

    /**
     *  Populate the cache.
     */
    self.populateCache = function(callback) {
		console.log("Populating cache");

        if (typeof self.zcache === "undefined") {
            self.zcache = {};
        }

        //  Local cache for static content.
        self.zcache['theme.html'] = fs.readFileSync('./public/theme.html');

        // callback
        if (typeof(callback)=="function"){
            callback();
        }
    };


    /**
     *  terminator === the termination handler
     *  Terminate server on receipt of the specified signal.
     *  @param {string} sig  Signal to terminate on.
     */
    self.terminator = function(sig){
        if (typeof sig === "string") {
           console.log('%s: Received %s - terminating application ...', Date(Date.now()), sig);
           process.exit(1);
        }
        console.log('%s: Node server stopped.', Date(Date.now()) );
    };


    /**
     *  Setup termination handlers (for exit and a list of signals).
     */
    self.setupTerminationHandlers = function(){
        //  Process on exit and signals.
        process.on('exit', function() { self.terminator(); });

        ['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT',
         'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGTERM'
        ].forEach(function(element, index, array) {
            process.on(element, function() { self.terminator(element); });
        });
    };

    /**
     *  Create the routing table entries + handlers for the application.
     */
    self.createRoutes = function() {
        console.log("Creating Routes");

        if (typeof self.routes === "undefined") {
            self.routes = {};
        }

        self.routes['/theme'] = function(req, res) {
            res.setHeader('Content-Type', 'text/html');
            res.send(self.zcache['theme.html']);
        };
    };


    /**
     *  Initialize the server (express) and create the routes and register
     *  the handlers.
     */
    self.initializeServer = function() {
        self.createRoutes();
        self.app = express();

        //  Add handlers for the app (from the routes).
        //  createRoutes() definitions need ot be called first
        for (var r in self.routes) {
            self.app.get(r, self.routes[r]);
        }

        // This is how to handle a URL via a get request to the cache
        self.app.get('/:handle', function(req,res){
            // Check the cache first
            if (typeof(self.zcache[req.params.handle])!="undefined"){
                res.setHeader('Content-Type', 'text/html');
                return res.send(self.zcache[req.params.handle]);
            }
        });

        // Serve all the files in the relative path
        self.app.get('/*', function(req,res) {

            // Check the cache first...
            if (typeof(self.zcache[req.url]) != "undefined" && self.zcache[req.url] != null) {
                res.statusCode = 200;
                res.write(self.zcache[req.url]);
                return res.end();
            }

            var fileLoc = path.resolve(self.staticBasePath);
            fileLoc = path.join(fileLoc, req.url);

            // Check the cache based on file location
            if (typeof(self.zcache[fileLoc]) != "undefined" && self.zcache[fileLoc] != null) {
                res.statusCode = 200;
                res.write(self.zcache[fileLoc]);
                return res.end();
            }

            // ...otherwise load the file
            fs.readFile(fileLoc, function(err, data) {
                if (err) {
                    res.writeHead(404, 'Not Found');
                    res.write('404: File Not Found!');
                    return res.end();
                }

                // (optional) Save to the cache
                // self.zcache[fileLoc] = data;

                res.statusCode = 200;
                res.write(data);
                return res.end();
            });
        });
    };


    /**
     *  Initializes the sample application.
     */
    self.initialize = function(callback) {
        self.setupVariables();
        self.populateCache(function(){
            self.setupTerminationHandlers();
            // Create the express server and routes.
            self.initializeServer();
            if (typeof(callback)=="function"){
                callback();
            }
        });
    };


    /**
     *  Start the server (starts up the sample application).
     */
    self.start = function() {
        //  Start the app on the specific interface (and port).
        self.app.listen(self.port, self.ipaddress, function() {
            console.log('%s: Node server started on %s:%d ...', Date(Date.now() ), self.ipaddress, self.port);
        });
    };
};


/**
 *  main():  Main code.
 */
var app = new App();
app.initialize(function(){
    app.start();
});

Now, from the command prompt run

node server.js

The console should read

Node server started on 127.0.0.1:8080 ...

You should be able to visit http://127.0.0.1:8080/theme.html