Four years ago I wrote a post on how to build a simple website in Node.js. Seeing as it’s still the most popular article on this blog, I thought I’d take a look at how things have evolved, what I would do differently today, and some additional nuggets of advice.
Initialise it
You can follow the code snippets in this post to build up the basis of a simple website or head over to this repo to check out the entire codebase.
mkdir simple-website-2016 && cd simple-website-2016
npm init
npm will prompt you for a bunch of info, so feed it something appropriate.
The same, but different
Express has stood the test of time and is still the module I’d reach for to build a simple website in Node.js. Over the last four years, unlike many long-lived modules, Express has trimmed the fat and pushed lots of functionality out of core and into external modules. This means it’s more lightweight than ever, and doesn’t load you up with a ton of bloat that you’ll never need. The only caveat of this is that you need to explicitly install certain bits which used to be bundled in, for instance the logger (now called morgan):
npm i --save express@4 morgan@1
Like Express, over the years my preference remains uneroded for Jade and Stylus as my template language and css preprocessor:
npm i --save jade@1 stylus@0
The biggest change is the approach to rendering/compilation of the Jade and Stylus. Previously, for rendering Jade templates the Express “view engine” was used. For Stylus, the compilation “middleware” was added to the server. Both of these mechanisms mean that the assets are rendered/compiled “on-demand” – i.e. when a request for the compiled assets reaches the server. There are a couple of problems with this:
- If you have an error (syntax or runtime) in your Jade/Stylus, you won’t find out until the a request is made to the server requiring those compiled assets. For frequently visited pages, this isn’t a huge problem, but you could have your website running for hours, even days before someone hits a page which renders an erroneous.
- The first request to hit the server incurs some latency while the assets are compiled. While subsequent requests will serve a cached asset and won’t trigger a recompile, they still have to wait for the filesystem when checking if any files were modified.
- Architecturally, this is a poor separation of concerns. A web server becomes a lot easier to test, maintain and reason about if its job is to serve content. Adding the responsibility of compiling static assets (tasks which makes more sense in a “build” process) introduces coupling and complexity.
The same, but different
Express has stood the test of time and is still the module I’d reach for to build a simple website in Node.js. Over the last four years, unlike many long-lived modules, Express has trimmed the fat and pushed lots of functionality out of core and into external modules. This means it’s more lightweight than ever, and doesn’t load you up with a ton of bloat that you’ll never need. The only caveat of this is that you need to explicitly install certain bits which used to be bundled in, for instance the logger (now called morgan):
npm i --save express@4 morgan@1
Like Express, over the years my preference remains uneroded for Jade and Stylus as my template language and css preprocessor:
npm i --save jade@1 stylus@0
The biggest change is the approach to rendering/compilation of the Jade and Stylus. Previously, for rendering Jade templates the Express “view engine” was used. For Stylus, the compilation “middleware” was added to the server. Both of these mechanisms mean that the assets are rendered/compiled “on-demand” – i.e. when a request for the compiled assets reaches the server. There are a couple of problems with this:
- If you have an error (syntax or runtime) in your Jade/Stylus, you won’t find out until the a request is made to the server requiring those compiled assets. For frequently visited pages, this isn’t a huge problem, but you could have your website running for hours, even days before someone hits a page which renders an erroneous.
- The first request to hit the server incurs some latency while the assets are compiled. While subsequent requests will serve a cached asset and won’t trigger a recompile, they still have to wait for the filesystem when checking if any files were modified.
- Architecturally, this is a poor separation of concerns. A web server becomes a lot easier to test, maintain and reason about if its job is to serve content. Adding the responsibility of compiling static assets (tasks which makes more sense in a “build” process) introduces coupling and complexity.
The Build Process
These days JavaScript is overloaded with tooling (we’re even guilty of throwing our hat into the ring with pliers), but remembering this is a “simple” website, I’m going to steer clear of any build tool in this article, and I would suggest you do too.
As an aside, I’m not just leaving build tools out of this article because they’re overkill for this small demo, I take this approach on any project I’m getting started with. My method with most software is to ignore it, try to get by without it, and only to reach for it if I identify a specific problem that it can solve well.
If your build process becomes unwieldy, a tool like grunt, gulp or pliers can help to organise things. But equally (especially with grunt and gulp) their configuration and setup can be equally unwieldy, and significantly bewildering.
Replace the scripts section of your package.json with the following:
{
"build-css": "stylus source/stylesheets/index.styl -o static/css",
"watch-css": "stylus source/stylesheets/index.styl -o static/css -w",
"clean": "rm -rf static/css && mkdir -p static/css",
"build": "npm run clean && npm run build-css",
"watch": "npm run clean && npm run watch-css & nodemon server -e js,jade",
"start": "node server"
}
Note: if you’re on Windows, the clean task needs to use different path separators…
rm -rf static\\css && mkdir -p static\\css
Thanks to Timothy Trowbridge for pointing this out.
I’ll explain each command in detail. The reason to put them in the package.json; is simply to give them aliases. The benefits are three-fold: npm allows you to use locally installed modules without typing node_modules/.bin; you don’t have to remember how to type the entire command each time; and it means that you can freely change the contents of the command (you could, for example, change the build-css command to use Sass instead of Stylus, and nobody else on your team would need to know – they continue building their css blissfully unaware).
Running commands is simple: npm run <name>
- npm run build-css – this uses the Stylus CLI to compile the index.styl stylesheet to css, and place it in the static/css directory
- npm run watch-css – this is exactly the same as the previous command, except that it will continue running until manually stopped, compiling the stylesheet any time the source files are change. This task essentially supersedes the stylus middleware I mentioned earlier.
- npm run clean – this removes any built files (currently only css, but it might later include browserify-ed JS) and creates any required directories.
- npm run build – this does everything required for the server to run correctly, which is just to run the clean and build-css commands
- npm run watch – this is the command that is most useful in development. It will watch the entire project for changes and recompile assets or restart the server accordingly.
- npm run start – this simply starts the server and does no watching at all.
There was one additional dependency introduced here – nodemon. It does a great job at watching files and restarting on change, so install it like so:
npm i --save nodemon@1.9
The Server
Insert the following code into server.js in the root of the project:
var express = require('express')
, logger = require('morgan')
, app = express()
, template = require('jade').compileFile(__dirname + '/source/templates/homepage.jade')
app.use(logger('dev'))
app.use(express.static(__dirname + '/static'))app.get('/', function (req, res, next) {
try {
var html = template({ title: 'Home' })
res.send(html) } catch (e) {
next(e)
}
})
app.listen(process.env.PORT || 3000, function () {
console.log('Listening on http://localhost:' + (process.env.PORT || 3000))
})
There’s not a huge amount of code there and hopeful it’s all pretty straightforward:
- Require all of the dependencies.
- Compile the Jade template.*
- Add the logger and static middleware to express.
- Add a route to serve the homepage. The route passes some data to the template, which renders some html. This is in a try/catch block because it is possible for Jade to throw runtime errors (for example, referencing an undefined variable). Any errors are handed off to the express error handler.
- Bind to port 3000 and service HTTP requests.
* The eagle-eyed among you will notice that the Jade compile call will do synchronous fs; operations and block the event loop. Blocking/synchronous operations are acceptable on startup since require() itself is synchronous and blocking. Once the process has started up and all require() calls have been resolved, it’s async all the way. Block at your peril!
Other than this JS, all you need to get a working site is a stylesheet at source/stylesheets/index.styl and a template and source/templates/homepage.jade. You can use the examples provided in the repo.
Run it
All that remains now is to boot it up! Use the command npm run watch to start the server and watch for changes. You should see a bunch of terminal output, including the following:
Listening on http://localhost:3000
Visit that link in your browser, and if you used my stylesheet and markup, you should have something that looks like this: (snazzy, eh?)
In summary
Four years ago, Node.js was transitioning from a fledgling to an adolescent platform. The proliferation and surge of module creation in the npm ecosystem was already massively underway. This meant that some of the things we choose to use were in a state of flux.
A lot of the tech in the modules we use hasn't changed fundamentally, but there has been a steady progression on their part – better performance, better security and api improvements, and a refining of processes on our part – how we choose to use them, and how they fit into our stack.
Get in touch if you want to find out more about how we could help you to build your applications using Node.js.