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:
config.js'use strict'module.exports = {staticPath: __dirname // process.cwd()}
Which our application will include via require('./config')
(extensions are optional).
The most basic way of accomplishing this, is to use fs.readFile
server-readfile.js'use strict'// Requiresconst httpUtil = require('http')const fsUtil = require('fs')const pathUtil = require('path')const urlUtil = require('url')const config = require('./config')// ServerhttpUtil.createServer(function (req, res) {const file = urlUtil.parse(req.url).pathnameconst path = pathUtil.join(config.staticPath, file)fsUtil.exists(path, function (exists) {if (!exists) {res.statusCode = 404return res.end('404 File Not Found')}fsUtil.readFile(path, function (error, data) {if (error) {console.log('Warning:', error.stack)res.statusCode = 500return res.end('500 Internal Server Error')}return res.end(data)})})}).listen(8080)
Test it: curl http://localhost:8080/server-static.js
However, readFile will read the entire file, then send the entire file. Take a moment to imagine how this not optimum.
The next most basic way, is to use a Readable Stream via fs.createReadStream.
server-stream.js'use strict'// Requiresconst httpUtil = require('http')const fsUtil = require('fs')const pathUtil = require('path')const urlUtil = require('url')const config = require('./config')// ServerhttpUtil.createServer(function (req, res) {const file = urlUtil.parse(req.url).pathnameconst path = pathUtil.join(config.staticPath, file)fsUtil.exists(path, function (exists) {if (!exists) {res.statusCode = 404return res.end('404 File Not Found')}const read = fsUtil.createReadStream(path)read.on('error', function (error) {console.log('Warning:', error.stack)res.statusCode = 500return res.end('500 Internal Server Error')})read.pipe(res)})}).listen(8080)
Test it: curl http://localhost:8080/server-static.js
Getting more advanced here. What about outputting the contents of directories too? For this, we can use fs.readdir
server-static.js'use strict'// Requiresconst httpUtil = require('http')const fsUtil = require('fs')const pathUtil = require('path')const urlUtil = require('url')const 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?// ServerhttpUtil.createServer(function (req, res) {const file = urlUtil.parse(req.url).pathnameconst path = pathUtil.join(config.staticPath, file)fsUtil.exists(path, function (exists) {if (!exists) {res.statusCode = 404return res.end('404 File Not Found')}fsUtil.stat(path, function (error, stat) {if (error) {console.log('Warning:', error.stack)res.statusCode = 500return res.end('500 Internal Server Error')}if (stat.isDirectory()) {fsUtil.readdir(path, function (error, files) {if (error) {console.log('Warning:', error.stack)res.statusCode = 500return res.end('500 Internal Server Error')}return res.end(files.join('\n'))})}else {const read = fsUtil.createReadStream(path)read.on('error', function (error) {console.log('Warning:', error.stack)res.statusCode = 500return res.end('500 Internal Server Error')})read.pipe(res)}})})}).listen(8080)
Test it: curl http://localhost:8080
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:
server-static-module.js'use strict'// Requiresconst httpUtil = require('http')const serveStatic = require('./serve-static')const config = require('./config')// ServerhttpUtil.createServer(function (req, res) {serveStatic(config.staticPath, req, res)}).listen(8080)// Can even do this, due to the simplicity// httpUtil.createServer(// serveStatic.bind(null, config.staticPath)// ).listen(8080)
One could come up with the following:
serve-static.js'use strict'// Requiresconst fsUtil = require('fs')const pathUtil = require('path')const urlUtil = require('url')// Serve staticmodule.exports = function (root, req, res, next) {const file = urlUtil.parse(req.url).pathnameconst path = pathUtil.join(root, file)fsUtil.exists(path, function (exists) {if (!exists) {if (next) return next()res.statusCode = 404return res.end('404 File Not Found')}fsUtil.stat(path, function (error, stat) {if (error) {console.log('Warning:', error.stack)res.statusCode = 500return res.end('500 Internal Server Error')}if (stat.isDirectory()) {fsUtil.readdir(path, function (error, files) {if (error) {console.log('Warning:', error.stack)res.statusCode = 500return res.end('500 Internal Server Error')}return res.end(files.join('\n'))})}else {const read = fsUtil.createReadStream(path)read.on('error', function (error) {console.log('Warning:', error.stack)res.statusCode = 500return 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.
server-static-custom.js'use strict'// Requiresconst httpUtil = require('http')const serveStatic = require('./serve-static')const config = require('./config')// ServerhttpUtil.createServer(function (req, res) {serveStatic(config.staticPath, req, res, function () {res.statusCode = 404res.end('404 Not Found. 🙁 \n')})}).listen(8080)
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.
Before we can install anything, we need to first initialise our directory as a node.js project:
npm init
That will create a package.json
file that will keep track of the dependencies we install.
We can install connect like so:
npm install --save connect
Using connect, our static file server example would become:
connect-static.js'use strict'// Requiresconst connect = require('connect')const config = require('./config')// Serverconst app = connect()// Middlewares// Use our local static middlewareapp.use(require('./serve-static').bind(null, config.staticPath))// Create the fallback middleware for when the route was not foundapp.use(function (req, res) {res.statusCode = 404res.end('404 Not Found. 🙁 \n')})// Listenapp.listen(8080)
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.
There is even existing middlewares for what we've just accomplished. We can install them like so:
npm install --save serve-static serve-index
And consume them like so:
connect-community.js'use strict'// Requiresconst connect = require('connect')const config = require('./config')// Serverconst app = connect()// Middlewares// Serve the files at the static pathapp.use(require('serve-static')(config.staticPath))// If a directory is requested, serve its index.html file if it has oneapp.use(require('serve-index')(config.staticPath))// If none of the previous middlewares matched, then fallback to 404app.use(function (req, res) {res.statusCode = 404res.end('404 Not Found. 🙁 \n')})// Listenapp.listen(8080)
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:
express-basic.js'use strict'// Requiresconst express = require('express')// Applicationconst app = express()// Routes// These make things easierapp.get('/', function (req, res) {res.send('hello world')})// Fallback middlewareapp.use(function (req, res) {res.send(404, '404 Not Found. 🙁 \n')// ^ this is different from connect})// Serverconst server = app.listen(8080)
It is important to note that the line
var server = app.listen(8080);
is the same as doingvar server = require('http').createServer(app).listen(8080);
. You will need to remember this when interfacing with other modules - some modules like to interface with the express instanceapp
while others like to interface with the node http server instanceserver
instead.
And this is what our static file server will look like with express:
express-static.js'use strict'// Requiresconst express = require('express')const config = require('./config')// Applicationconst app = express()// Middlewares// Use our local oneapp.use(require('./serve-static').bind(null, config.staticPath))// Use the prexisting ones// app.use(require('serve-static')(config.staticPath))// app.use(require('serve-index')(config.staticPath))// Fallback middlewareapp.use(function (req, res) {res.send(404, '404 Not Found. 🙁 \n')})// Serverconst server = app.listen(8080)
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.