title layout thread date author categories tags excerpt
Master Koa middleware post 181 2017-11-10 Joe Jiang documents
Koa JavaScript Middleware Nodejs Koajs Expressjs
Koa2 was recently released with Node quietly implementing the async-await usage. Express still seems to have the upper hand in the popularity contest, but I’ve been happily using it since Koa2 was released, and always dread going back to old projects to use Express…

Koa2 was recently released with Node quietly implementing the async-await usage. Express still seems to have the upper hand in the popular race, but I’ve been happily using it since Koa2 was released, and always dread going back to old projects to use Express.

I popped in and out of Koa Gitter to answer questions, and the most I ever answered was about the magic of the Koa middleware system, so I decided to write about it.

There are a lot of Koa novices who have used Express, so I’ll do a lot of comparisons between the two.

This article is aimed at newcomers to Koa and those who are considering using Koa in their next project.

The basic concept

Let’s start with the most important. In Koa and Express, everything about HTTP requests is done inside the middleware, and the most important thing is to understand the concept of middleware continuation passing. It sounds strange, but it’s not. The idea is that once the middleware has done its thing, it can choose to 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'))Copy the code

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'))Copy the code

Let’s use the curl command to test both:

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

Both examples do the same thing, and both print the same output on the terminal:

Setting status
Setting body
Copy the code

This indicates that the middleware is top-down in both cases.

The biggest difference here is that the Express middleware chain is based on callbacks, whereas Koa is based on promises.

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

Express

$ curl http://localhost:3001
 
Copy the code

. It never ends. This is because in Express, you have to choose between calling next() and sending response — otherwise the request will never complete.

Koa

$ curl http://localhost:3002
OK
Copy the code

Ah, all koAS will complete the request, it has status code information, but no body information. So the second middleware is not called.

But there is one other thing that matters to Koa. If you call next(), you have to wait for it!

Here’s the best 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'
})Copy the code

Let’s see what happens.

$ curl http://localhost:3002
OK
Copy the code

Well, we called next(), but no body information was passed? This is because Koa terminates the request after the middleware Promise chain has been resolved. This means that the response is sent to the client before we set ctx.body!

Another thing to understand is that if you are using pure promise.then () instead of async-await, your middleware needs to return a Promise. When the returned promise is resolved, Koa restores the previous middleware at this point.

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

A better example of using pure 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'
  })
})Copy the code

Koa Middleware – Game changing features

In the previous chapter I wrote:

Koa will restore the previous middleware at this point

This may disappoint you, but allow me to explain.

In Express, a middleware can only do something about it before calling next(), not after. Once you call next(), the request never touches the middleware again. This can be a bit disappointing, and people (including the Express authors themselves) have found some clever solutions, such as watching the response stream when the header gets written, but it’s just awkward for the average user.

For example, implementing a middleware that records the time it takes to complete a request and sends it to the X-responseTime header requires a “before the next call” code point and a “after the next call” code point. In Express, it is implemented using stream observation techniques.

Let’s 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'
})Copy the code

Eight rows, that’s all you need. No tricky flow sniffing, just async-await code that looks pretty good. Let’s run it! The -i tag tells Curl and also shows us the header of the response.

$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 KoaCopy the code

Very good! We found the response time in the HTTP header. Let’s look at the logs on the terminal again and see in what order the logs are printed.

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

See, there you go. Koa gives us complete control over the middleware process. Implementing things like authentication and error handling would be easy!

Error handling

This is one of my favorite things about Koa, which is supported by the powerful middleware Promise chain detailed above.

For good measurement, let’s look at how we do this in Express.

Express

Error handling is done in a specially signed middleware that must be added to 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) })Copy the code

This is a prime example. If you’re dealing with asynchronous errors from callbacks or promises, this can get tedious. Such as:

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()
})Copy the code

I am fully aware that it is easier to handle callback hell using modules, just to prove that simple error handling in Express becomes unwieldy, not to mention that you need to consider asynchronous errors, synchronous errors, etc.

Koa

Error handling is also done using Promises. Koa always wrapped next() in a promise for us, so we didn’t even have to worry about asynchrony and synchronization errors.

The error-handling middleware runs at the top because it “bypasses” every subsequent middleware. This means that any errors added after error handling are caught (yes, feel it!).

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` })Copy the code

Yeah, a try-catch. How appropriate is * for error handling! * The non-async-await mode is as follows:

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

Let’s start with a mistake:

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

Console output as expected:

Error handler: can only greet "world"
Copy the code

routing

Unlike Express, Koa has almost nothing available. There is no BodyParser and no routing.

There are many routing options available in Koa, such as 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'
  }])
})Copy the code

Koa

In this example I chose koa-Router because it is the route I am using.

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())Copy the code

conclusion

Koa is cool. Complete control based on the middleware chain, and the fact that it’s based on promises makes everything easier to do. Instead of if (err) return next(err) everywhere, just promise.

With super-powerful error handlers, we can throw errors that more elegantly exit the path of our code (think validation errors, business logic violations).

Here is a list of middleware I use most often (in no particular order) :

  • koa-compress
  • koa-respond
  • kcors
  • koa-convert
  • koa-bodyparser
  • koa-compose
  • koa-router

It’s good to know that not all middleware is available based on Koa 2, and then they can be converted at run time via KOA-convert, so don’t worry.

The original link

Mastering Koa Middleware, Twitter @JeffiJoe