In this chapter, we will build an API to power our own Hacker News! While technically this wouldn't be very different from the previous chapters, we will use a different framework altogether, Koa.js (http://koajs.com/).
Koa.js is a new web framework designed by the team behind Express. Why did they create a new framework? Because it is designed from the bottom up, with a minimalistic core for more modularity, and to make use of the new generator syntax, proposed in ECMAScript 6, but already implemented in node 0.11.
An alternative to node 0.11 would be io.js, which at the time of writing reached version 1.0, and also implements ES6 goodies (forked from Node.js and maintained by a handful of node core contributors). In this chapter, we will stick to node 0.11. (When this book went to print, node 0.12 was finally released and is the latest stable version of node.)
One of the main benefits of the generator syntax is that you can very elegantly avoid callback hell, without the use of complicated promise patterns. You can write your APIs even more cleanly than ever before. We'll go over the subtleties as well as some caveats that come with the bleeding edge.
Some things we will cover in this chapter are as follows:
Generator functions are at the core of Koa.js, so let's dive right into dissecting this beast. Generators allow adept JavaScript users to implement functions in completely new ways. Koa.js makes use of the new syntax to write code in a synchronous-looking fashion while maintaining the performance benefits of an asynchronous flow.
The following defines a simple generator function in src/helloGenerator.js
(note the asterisk syntax):
module.exports = function *() { return 'hello generator'; };
To use Mocha with Koa.js:
co-mocha
to add generator support, requiring once at the first line of each test file is the safe way to do it. Now you can pass generator functions to Mocha's it
function as follows:require('co-mocha'), var expect = require('chai').expect; var helloGenerator = require('../src/helloGenerator'), describe('Hello Generator', function() { it('should yield to the function and return hello', function *() { var ans = yield helloGenerator(); expect(ans).to.equal('hello generator'), }); });
--harmony-generators
flag as you run Mocha:./node_modules/mocha/bin/mocha --harmony-generators
Note the magic use of the yield
keyword. The yield
keyword can only be used within a Generator
function, and works somewhat similar to return
, expecting a single value to be passed, that can also be a generator function (also accepts other yieldables—more on that later), and yields the process to that function.
When a function*
is passed, the execution flow will wait until that function returns before it continues further down. In essence, it would be equivalent to the following callback pattern:
helloGenerator(function(ans) { expect(ans).to.equal('hello generator'), });
Much cleaner, right? Compare the following code:
var A = yield foo(); var B = yield bar(A); var C = yield baz(A, B);
With the nasty callback hello if we didn't have generator functions:
var A, B, C; foo(function(A) { bar(A, function(B) { baz(A, B, function(C) { return C; }); }); });
Another neat advantage is super clean error handling, which we will get into later.
The preceding example is not too interesting because the helloGenerator()
function is a synchronous function anyway, so it would've worked the same, even if we didn't use generator functions!
helloGenerator.js
to the following:module.exports = function *() { setTimeout(function(){ return 'hello generator'; }, 1000); }
Wait! Your test is failing?! What is going on here? Well, yield
should have given the flow to the helloGenerator()
function, let it run asynchronously, and wait until it is done before continuing. Yet, ans
is undefined. And nobody is lying.
The reason why it is undefined is because the generator()
function returns immediately after calling the setTimeout
function, which is set to ans
. The message that should have returned from within the setTimeout
function is broadcast into the infinite void, nowhere to be seen, ever again.
One thing to keep in mind with generator functions is that once you use a generator function, you better commit, and not resort to callbacks down the stack! Recall that we mentioned that yield
expects a generator function. The setTimeout
function is not a generator function, so what do we do? The yield
method can also accept a Promise or a Thunk (more on this later).
setTimeout()
function isn't a Promise, so we have two options left; we can thunkify the function, which basically takes a normal node function with a callback pattern and returns a Thunk, so we can yield to it; alternatively, we use co-sleep, which is basically a minuscule node package that has done it for you as follows:module.exports = sleep; function sleep(ms) { return function (cb) { setTimeout(cb, ms); }; }
co-sleep
. Generally a good idea to reuse what's available is to just do a quick search in the npm registry. There are numerous co
packages out there!var sleep = require('co-sleep'), module.exports = function *() { yield sleep(1000); return 'hello generator'; }
co
library is what's under the hood of Koa.js, giving it the generator-based control flow goodies. If you want to use this sort of flow outside Koa.js, you can use something like this:var co = require('co'), var sleep = require('co-sleep'), co(function*(){ console.log('1'), yield sleep(10); console.log('3'), }); console.log('2'),
You should be familiar by now with the middlewares in Express. We used them a lot to dry out code, especially for validation and authentication. In Express, middleware is placed between the server that receives the request and the handler that responds to a request. The request flows one way, until it terminates at res.send
or something equivalent.
In Koa.js, everything is a middleware, including the handler itself. As a matter of fact, a Koa.js application is just an object, which contains an array of middleware generator functions! The request flows all the way down the stack of middlewares, and back up again. This is best explained with a simple example:
var koa = require('koa'), var app = koa(); app.use(function *(next){ var start = new Date(); yield next; var ms = new Date() - start; this.set('X-Response-Time', ms + 'ms'), }); app.use(function *(){ this.body = 'Hello World'; }); app.listen(3000);
Here we have a Koa.js application with two middlewares. The first middleware adds an X-Response-Time
header to the response, whereas the second middleware simply sets the response body to Hello
World
for each request. The flow is as follows:
3000
.Date
object is created and assigned to start
.body
on the Context to Hello
World
.To run this app, we can use the following command:
node --harmony app.js
A Koa.js Context is created for each incoming request. Within each middleware, you can access the Context using the this
object. It includes the Request and Response object in this.request
and this.response
, respectively, although most methods and accessors are directly available from the Context.
The most important property is this.body
, which sets the response body. The response status is automatically set to 200
when the response body is set. You may override this by setting this.status
manually.
Another very useful syntactic sugar is this.throw
, which allows you to return an error response by simply calling this.throw(400)
, or if you want to override the standard HTTP error message, you may pass a second argument with the error message. We will get to Koa.js slick error handling later in this chapter.
Now that we've got the basics down, let's start building a Hacker News API!
The following code describes the straightforward link document model in src/models/links.js
:
var mongoose = require('mongoose'), var schema = new mongoose.Schema({ title: { type: String, require: true }, URL: { type: String, require: true }, upvotes: { type: Number, require: true, 'default': 0 }, timestamp: { type: Date, require: true, 'default': Date.now } }); schema.statics.upvote = function *(linkId) { return yield this.findByIdAndUpdate(linkId, { $inc: { upvotes: 1 } }).exec(); }; var Links = mongoose.model('links', schema); module.exports = Links;
Note that this is pretty much identical to how you would define a model in Express, with one exception: the upvotes
static method. Since findByIdAndUpdate
is an asynchronous I/O operation, we need to make sure that we yield
to it, so as to make sure we wait for this operation to complete, before we continue the execution.
Earlier we noted that not only generator functions can be yielded to; it also accepts Promises, which is awesome, because they are quite ubiquitous. Using Mongoose, for example, we can turn Mongoose query instances into Promises by calling the exec()
method.
With the link model in place, let's set up some routes in src/routes/links.js
:
var model = require('../models/links'), module.exports = function(app) { app.get('/links', function *(next) { var links = yield model.find({}).sort({upvotes: 'desc'}).exec(); this.body = links; }); app.post('/links', function *(next) { var link = yield model.create({ title: this.request.body.title, URL: this.request.body.URL }); this.body = link; }); app.delete('/links/:id', function *(next) { var link = yield model.remove({ _id: this.params.id }).exec(); this.body = link; }); app.put('/links/:id/upvote', function *(next) { var link = yield model.upvote(this.params.id); this.body = link; }); };
This should start to look familiar. Instead of function handlers with the signature (req, res
) that we are used to in Express, we simply use middleware generator functions and set the response body in this.body
.
Now that we have our model and routes defined perform the following steps:
src/app.js
:var koa = require('koa'), app = koa(), bodyParser = require('koa-body-parser'), router = require('koa-router'), // Connect to DB require('./db'), app.use(bodyParser()); app.use(router(app)); require('./routes/links')(app); module.exports = app;
/app.js
as given in the following:var app = require('./src/app.js'), app.listen(3000); console.log('Koa app listening on port 3000'),
This just loads the app and starts an HTTP server, which listens on port 3000
. Now to start the server, make sure you use the --harmony-generators
flag. You should now have a working Koa API to power a Hacker News-like website!