Simplifying Node with Koa
07 Jul 2015 · CommentsI really enjoy writing web applications in Node JS, but sometimes the asynchronous programming style makes my head hurt! This code sample is from my current side project, commentator. It retrieves comments stored as Markdown files from the local file system and returns them as a JSON array:
var folder = path.join('data', req.url);
var files = fs.readdirSync(folder);
var comments = [];
for (var i = 0; i < files.length; i++) {
var file = path.join(folder, files[i]);
var markdown = fs.readFileSync(file, 'UTF-8');
var comment = parse(file, markdown);
comments.push(comment);
}
res.send(comments);
Dead simple, right? It looks for files in the requested folder, loops through each file and reads the contents, then parses the contents into a comment and adds it to an array.
But show this simple, readable code to an experienced Node programmer and
they’ll laugh you out of the room. They’ll point out that using
fs.readdirSync
or fs.readFileSync
in a web application will tie up your
single JavaScript thread with lengthy file system operations. The application
won’t be able to serve requests from multiple users while it’s reading from the
file system leading to a very slow website when placed under load.
To allow this code to scale we need to use the asynchronous version of the file system methods… but what does that look like?
var folder = path.join('data', req.url);
fs.readdir(folder, function (err, files) {
if (err) throw err;
var comments = [];
for (var i = 0; i < files.length; i++) {
var file = path.join(folder, files[i]);
fs.readFile(file, 'UTF-8', function (err, markdown) {
if (err) throw err;
var comment = parse(file, markdown);
comments.push(comment);
if (comments.length == files.length) {
res.send(comments);
}
});
}
});
Yuck! My original 10 lines of code have increased to 16; the maximum indentation
has gone up to 4 levels; and I’ve had to sprinkle the code with
if (err) throw err;
statements to manually check for, and re-throw errors! 🙀
There’s also complexity around when the response is ready to be sent. The code
loops over the files and kicks off asynchronous readFile
operations in
parallel. The callbacks may come out of order, so I have added an if
statement into each one to check if all the comments have been added before
sending the response.
Enter Koa. It’s a new web framework from the team behind Express, so it has some pedigree. Koa allows you to write the following:
app.use(function *() {
var folder = path.join('data', this.request.url);
var files = yield fs.readdir(folder);
var comments = [];
for (var i = 0; i < files.length; i++) {
var file = path.join(folder, files[i]);
var markdown = yield fs.readFile(file, 'UTF-8');
var comment = parse(file, markdown);
comments.push(comment);
}
this.body = comments;
});
This code is identical to the simple version except that it uses asynchronous
file system operations preceded by the yield
keyword. Koa cleverly
abuses uses a new feature of JavaScript called generators to
pause the code mid-flow, wait for the asynchronous operation to complete, then
continue executing.
There is one catch - you can only yield asynchronous operations that return a promise. However, the file system operations in the code above use the standard Node callback pattern, so I’ve added the following wrapper to convert the callbacks to promises:
var fs = require('fs'),
q = require('q');
module.exports.readdir = q.denodeify(fs.readdir);
module.exports.readFile = q.denodeify(fs.readFile);
Hopefully I’ve convinced some of you to give Koa a try. I’m excited about how the code is shaping up in commentator, and since Koa is developed by the team who wrote Express - I have a feeling that it may be here to stay.