Mastering Koa Middleware

Jeff Hansen
Netscape
Published in
7 min readMar 31, 2017

Koa v2 recently dropped as Node made async-await generally available without a flag. Express still seems to be winning the popularity contest, but I’ve been using Koa happily since v2 was announced and always dread returning to Express for older projects.

I hang out in the Koa Gitter channel every now and then to answer questions and the ones I get the most are related to the magical Koa middleware system, so I thought I’d do a writeup on the matter.

A lot of Koa newcomers have worked with Express before, so I’ll be doing a lot of comparisons between the two.

This article is aimed at Koa newcomers, and people who are considering Koa for their next project.

The Basics

Let’s start with the essentials. In both Koa and Express, everything that deals with an HTTP request will be done inside middleware. The most important concept to understand is middleware continuation passing. This sounds awfully fancy, but it really isn’t. The idea is that once a middleware is done doing its thing, it can optionally call the next middleware in the chain.

Express

const express = require('express')const app = express()// Middleware 1
app.use((req, res, next) => {
res.status(200)
console.log('Setting status')
// Call the next middleware
next()
})
// Middleware 2
app.use((req, res) => {
console.log('Setting body')
res.send(`Hello from Express`)
})
app.listen(3001, () => console.log('Express app listening on 3001'))

Koa

const Koa = require('koa')const app = new Koa()// Middleware 1
app.use(async (ctx, next) => {
ctx.status = 200
console.log('Setting status')
// Call the next middleware, wait for it to complete
await next()
})
// Middleware 2
app.use((ctx) => {
console.log('Setting body')
ctx.body = 'Hello from Koa'
})
app.listen(3002, () => console.log('Koa app listening on 3002'))

Let’s hit both of them with curl:

$ curl http://localhost:3001
Hello from Express
$ curl http://localhost:3002
Hello from Koa

Both examples do the same thing, and both log the same output to the terminal:

Setting status
Setting body

This shows that in both cases, middleware runs top to bottom.

The big difference here is that the Express middleware chain is callback-based, while Koa’s is Promise-based.

Let’s see what happens if we omit calling next() in both examples.

Express

$ curl http://localhost:3001

… it never completes. This is because in Express, you have to either call next() or send a response — else the request won’t complete.

Koa

$ curl http://localhost:3002
OK

Ah, so the Koa app will finish the request, but without any body. It did set the status code though. So the 2nd middleware was not called.

But there’s one more thing that is crucial to Koa. If you do call next(), you must wait for it!

That is best illustrated with the following example:

// Simple Promise delay
function delay (ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
app.use(async (ctx, next) => {
ctx.status = 200
console.log('Setting status')
next() // forgot await!
})
app.use(async (ctx) => {
await delay(1000) // simulate actual async behavior
console.log('Setting body')
ctx.body = 'Hello from Koa'
})

Let’s see what happens.

$ curl http://localhost:3002
OK

Hmm, we called next(), but no body was sent? That’s because Koa ends the request once the middleware Promise chain is resolved. That means the response was sent to the client before we got to set ctx.body!

Another gotcha if you are using plain Promise.then() instead of async-await is that the middleware should return a promise. When the returned promise resolves, that’s when Koa will resume the previous middleware.

app.use((ctx, next) => {
ctx.status = 200
console.log('Setting status')
// need to return here, not using async-await
return next()
})

A better example of using plain promises:

// We don't call `next()` because
// we don't want anything else to happen.
app.use((ctx) => {
return delay(1000).then(() => {
console.log('Setting body')
ctx.body = 'Hello from Koa'
})
})

Koa middleware — the game-changing feature

In the previous section I wrote:

That’s when Koa will resume the previous middleware

And that might have thrown you off a bit. Allow me to explain.

In Express, a middleware can only do useful things before calling next(), not after. Once you call next() that request will never touch the middleware ever again. That can be kind of a bummer. People (including Express authors themselves) have found clever workarounds, like watching the response stream for when headers get written, but for the average consumer that just feels awkward.

For example, to implement a middleware that records the amount of time it takes to complete the request and send it in a X-ResponseTime header will require a “before calling next” code point and a “after calling next” code point. In Express, it’s implemented with the stream-watching technique.

Let’s try to implement it in Koa.

async function responseTime (ctx, next) {
console.log('Started tracking response time')
const started = Date.now()
await next()
// once all middleware below completes, this continues
const ellapsed = (Date.now() - started) + 'ms'
console.log('Response time is:', ellapsed)
ctx.set('X-ResponseTime', ellapsed)
}
app.use(responseTime)app.use(async (ctx, next) => {
ctx.status = 200
console.log('Setting status')
await next()
})
app.use(async (ctx) => {
await delay(1000)
console.log('Setting body')
ctx.body = 'Hello from Koa'
})

8 lines. That’s all it took. No funky stream sniffing, just great-looking async-await code. Let’s hit it! The -i flag tells curl to show us the response headers as well.

$ curl -i http://localhost:3002
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 14
X-ResponseTime: 1001ms
Date: Thu, 30 Mar 2017 12:52:48 GMT
Connection: keep-alive
Hello from Koa

Nice! We got our response time in an HTTP header. Let’s check our terminal for logs, to see in what order our console logs were written.

Started tracking response time
Setting status
Setting body
Response time is: 1001ms

And there you have it. Koa gives us full control over the middleware flow. Implementing things like authentication and error handling are going to be super easy!

Error handling

This is my absolute favorite thing about Koa, and it’s enabled by the kick-ass middleware Promise chain I detailed above.

For good measure, let’s look at how we’d do it in Express.

Express

Error handling is done in a special-signature middleware and it must be added at the end of the chain to work.

app.use((req, res) => {
if (req.query.greet !== 'world') {
throw new Error('can only greet "world"')
}
res.status(200)
res.send(`Hello ${req.query.greet} from Express`)
})
// Error handler
app.use((err, req, res, next) => {
if (!err) {
next()
return
}
console.log('Error handler:', err.message)
res.status(400)
res.send('Uh-oh: ' + err.message)
})

This is a best-case example scenario. If you are dealing with async errors from callbacks or Promises, it becomes extremely verbose. For example:

app.use((req, res, next) => {
loadCurrentWeather(req.query.city, (err, weather) => {
if (err) {
return next(err)
}

loadForecast(req.query.city, (err, forecast) => {
if (err) {
return next(err)
}

res.status(200).send({
weather: weather,
forecast: forecast
})
})
})

next()
})

I am fully aware of modules that make dealing with callback-hell easier, this is just to demonstrate that plain error handling in Express becomes unwieldy. Not to mention you need to account for async errors as well as sync errors.

Koa

Error handling is done using promises as well. Koa will always wrap next() in a promise for us, so we don’t even have to worry about async vs sync errors.

Error-handling middleware goes at the top because it “wraps around” every subsequent middleware. That means any error thrown in middleware added after the error handling will be caught (yes, feel the power!)

app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.status = 400
ctx.body = `Uh-oh: ${err.message}`
console.log('Error handler:', err.message)
}
})
app.use(async (ctx) => {
if (ctx.query.greet !== 'world') {
throw new Error('can only greet "world"')
}

console.log('Sending response')
ctx.status = 200
ctx.body = `Hello ${ctx.query.greet} from Koa`
})

Yes. A try-catch. For error handling. How fitting! The non-async-await way would be:

app.use((ctx, next) => {
return next().catch(err => {
ctx.status = 400
ctx.body = `Uh-oh: ${err.message}`
console.log('Error handler:', err.message)
})
})

Let’s try triggering an error.

$ curl http://localhost:3002?greet=jeff
Uh-oh: can only greet "world"

And the console output as expected:

Error handler: can only greet "world"

Routing

Unlike Express, Koa comes with almost nothing out of the box. No bodyparser, and no router either.

There’s a bunch of options for routing in Koa, like koa-route and koa-router. I prefer the latter.

Express

Routing in Express is built in.

app.get('/todos', (req, res) => {
res.status(200).send([{
id: 1,
text: 'Switch to Koa'
}, {
id: 2,
text: '???'
}, {
id: 3,
text: 'Profit'
}])
})

Koa

For this example I’ve chosen koa-router because it’s what I use.

const Router = require('koa-router')
const router = new Router()
router.get('/todos', (ctx) => {
ctx.status = 200
ctx.body = [{
id: 1,
text: 'Switch to Koa',
completed: true
}, {
id: 2,
text: '???',
completed: true
}, {
id: 3,
text: 'Profit',
completed: true
}]
})
app.use(router.routes())// makes sure a 405 Method Not Allowed is sent
app.use(router.allowedMethods())

Conclusion

Koa is awesome. Having full control over the middleware chain and the fact that it’s all promise-based makes everything so much easier to work with. No more if (err) return next(err) all over, just promises.

With the super-robust error handler, we are able to throw errors to break away from our code’s happy path more elegantly (think validation errors, business logic violations).

Here’s a list of middleware I use often (in no particular order):

Good to know: not all middleware is Koa 2-ready, however they can be converted at runtime using koa-convert so don’t worry.

I hope you found this article helpful. If you did, please click the recommend button. That would be great! :)

You can find me on Twitter: @Jeffijoe

Translations

--

--

Jeff Hansen
Netscape

Full-stack JavaScript engineer straight outta Denmark, making taxes suck less @Taxfyle. Created Awilix (JS library), https://SkyClip.co (service)