title | layout | thread | date | author | categories | tags | excerpt | ||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Master Koa middleware | post | 181 | 2017-11-10 | Joe Jiang | documents |
|
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 Koa
Copy 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