About Bevry

Bevry's Learning Centre

  doing everything we can to empower developers

Static File Server

One popular use case is to serve files directly from a directory on our machine.

For this, we will setup a configuration module at config.js that contains:

module.exports = {
    staticPath: __dirname  // process.cwd()
}

Which our application will include via require('./config') (extensions are optional).

Read File

The most basic way of accomplishing this, is to use fs.readFile

// Requires
var httpUtil = require('http')
var fsUtil = require('fs')
var pathUtil = require('path')
var urlUtil = require('url')
var config = require('./config')

// Server
httpUtil.createServer(function (req, res) {
    var file = urlUtil.parse(req.url).pathname
    var path = pathUtil.join(config.staticPath, file)
    fsUtil.exists(path, function (exists) {
        if ( !exists ) {
            res.statusCode = 404
            return res.end('404 File Not Found')
        }
        fsUtil.readFile(path, function (error, data) {
            if ( error ) {
                console.log('Warning:', error.stack)
                res.statusCode = 500
                return res.end('500 Internal Server Error')
            }
            return res.end(data)
        })
    })
}).listen(8000)

Test it: curl http://localhost:8000/server-static.js

However, readFile will read the entire file, then send the entire file. Take a moment to imagine how this not optimum.

Streams

The next most basic way, is to use a Readable Stream via fs.createReadStream.

// Requires
var httpUtil = require('http')
var fsUtil = require('fs')
var pathUtil = require('path')
var urlUtil = require('url')
var config = require('./config')

// Server
httpUtil.createServer(function (req, res) {
    var file = urlUtil.parse(req.url).pathname
    var path = pathUtil.join(config.staticPath, file)
    fsUtil.exists(path, function (exists) {
        if ( !exists ) {
            res.statusCode = 404
            return res.end('404 File Not Found')
        }
        var read = fsUtil.createReadStream(path)
        read.on('error', function (error) {
            console.log('Warning:', error.stack)
            res.statusCode = 500
            return res.end('500 Internal Server Error')
        })
        read.pipe(res)
    })
}).listen(8000)

Test it: curl http://localhost:8000/server-static.js

Directories

Getting more advanced here. What about outputting the contents of directories too? For this, we can use fs.readdir

// Requires
var httpUtil = require('http')
var fsUtil = require('fs')
var pathUtil = require('path')
var urlUtil = require('url')
var config = require('./config')

// @TODO
// This is getting a bit big, how can we refactor this?
// Can we abstract it out?
// What considerations do we need to take into account?
// How would we add additional actions if we abstract?

// Server
httpUtil.createServer(function (req, res) {
    var file = urlUtil.parse(req.url).pathname
    var path = pathUtil.join(config.staticPath, file)
    fsUtil.exists(path, function (exists) {
        if ( !exists ) {
            res.statusCode = 404
            return res.end('404 File Not Found')
        }
        fsUtil.stat(path, function (error, stat) {
            if ( error ) {
                console.log('Warning:', error.stack)
                res.statusCode = 500
                return res.end('500 Internal Server Error')
            }

            if ( stat.isDirectory() ) {
                fsUtil.readdir(path, function (error, files) {
                    if ( error ) {
                        console.log('Warning:', error.stack)
                        res.statusCode = 500
                        return res.end('500 Internal Server Error')
                    }
                    return res.end(files.join('\n'))
                })
            }
            else {
                var read = fsUtil.createReadStream(path)
                read.on('error', function (error) {
                    console.log('Warning:', error.stack)
                    res.statusCode = 500
                    return res.end('500 Internal Server Error')
                })
                read.pipe(res)
            }
        })
    })
}).listen(8000)

Test it: curl http://localhost:8000

Applying Abstractions

Considering the static file server example is incredibly common. Lets think of ways we can abstract out the serving of static files so we can re-use that functionality easily.

Desiring a solution like:

// Requires
var httpUtil = require('http')
var serveStatic = require('./serve-static')
var config = require('./config')

// Server
httpUtil.createServer(function (req, res) {
    serveStatic(config.staticPath, req, res)
}).listen(8000)

// Can even do this, due to the simplicity
// httpUtil.createServer(
//     serveStatic.bind(null, config.staticPath)
// ).listen(8000)

// @TODO
// Why does the bind solution work?

One could come up with the following:

// Requires
var fsUtil = require('fs')
var pathUtil = require('path')
var urlUtil = require('url')

// Serve static
module.exports = function (root, req, res, next) {
    var file = urlUtil.parse(req.url).pathname
    var path = pathUtil.join(root, file)
    fsUtil.exists(path, function (exists) {
        if ( !exists ) {
            if ( next )  return next()
            res.statusCode = 404
            return res.end('404 File Not Found')
        }
        fsUtil.stat(path, function (error, stat) {
            if ( error ) {
                console.log('Warning:', error.stack)
                res.statusCode = 500
                return res.end('500 Internal Server Error')
            }

            if ( stat.isDirectory() ) {
                fsUtil.readdir(path, function (error, files) {
                    if ( error ) {
                        console.log('Warning:', error.stack)
                        res.statusCode = 500
                        return res.end('500 Internal Server Error')
                    }
                    return res.end(files.join('\n'))
                })
            }
            else {
                var read = fsUtil.createReadStream(path)
                read.on('error', function (error) {
                    console.log('Warning:', error.stack)
                    res.statusCode = 500
                    return res.end('500 Internal Server Error')
                })
                read.pipe(res)
            }
        })
    })
}

Note the introduction of a next callback. This allows us to chain additional or custom functionality.

// Requires
var httpUtil = require('http')
var serveStatic = require('./serve-static')
var config = require('./config')

// Server
httpUtil.createServer(function (req, res) {
    serveStatic(config.staticPath, req, res, function () {
        res.statusCode = 404
        res.end('404 Not Found. 🙁 \n')
    })
}).listen(8000)

Introduction to Middlewares

In our previous example of writing the serveStatic what we effectively did was create a middleware, albiet a basic middleware but a middleware nonetheless. Naturally, there are already plenty of other middlewares published as modules by other people. This is helped by the middleware framework Connect originally by TJ Holowaychuk.

We can install connect like so:

npm install --save connect

Using connect, our static file server example would become:

// Requires
var connect = require('connect')
var config = require('./config')

// Server
var app = connect()


// Middlewares

// Use our local one
// app.use(require('./serve-static').bind(null, config.staticPath))

// Use a custom made one
app.use(require('serve-static')(config.staticPath))
app.use(require('serve-index')(config.staticPath))

// Do our fallback
app.use(function (req, res) {
    res.statusCode = 404
    res.end('404 Not Found. 🙁 \n')
})


// Listen
app.listen(8000)

Much simpler. Notice how the middlewares are like a waterfall, it hits the first, then if the first doesn't know what to do, the logic will flow through to the next middleware (accomplished by the previous middleware calling the next callback inside it).

Connect has plenty of other middlewares available for it. Official middlewares for connect are listed on its website with 3rd party middleware listed on github. You'll want to check them out.

Introduction to Web Frameworks

There are a few web frameworks for node. The most used is Express originally by TJ Holowaychuk.

Perhaps by now you would have noticed the amount of individuals mentioned who have created awesome things, this is very much the case, anyone can have an impact.

Express can be thought of as a layer that sits ontop of connect and node's http module. It provides its own middleware and uses its own request and response objects that inherit from those of node's http module. The benefit over just using connect are:

  • Addition of routing
  • Common functionality provided through a friendly syntax

We can install express like so:

npm install --save express

The following is what a simple hello world server would look like with express:

// Requires
var express = require('express')

// Application
var app = express()

// Routes
// These make things easier
app.get('/', function (req, res) {
  res.send('hello world')
})

// Fallback middleware
app.use(function (req, res) {
    res.send(404, '404 Not Found. 🙁 \n')
    // ^ this is different from connect
})

// Server
var server = app.listen(8000)

It is important to note that the line var server = app.listen(3000); is the same as doing var server = require('http').createServer(app).listen(80);. You will need to remember this when interfacing with other modules - some modules like to interface with the express instance app while others like to interface with the node http server instance server instead.

And this is what our static file server will look like with express:

// Requires
var express = require('express')
var config = require('./config')

// Application
var app = express()

// Middlewares

// Use our local one
app.use(require('./serve-static').bind(null, config.staticPath))

// Use a custom made one
// app.use(require('serve-static')(config.staticPath))
// app.use(require('serve-index')(config.staticPath))

// Fallback middleware
app.use(function (req, res) {
    res.send(404, '404 Not Found. 🙁 \n')
})


// Server
var server = app.listen(8000)

Notice the nicer syntax for setting the status code of the response. Before it was res.statusCode = 404; now it is the first argument of res.send - this is what we meant by express providing a nicer syntax :)