Revisiting Koa Middlewares

Table of Contents
What is Koa?
Koa.js is a Node.js framework, similar to https://expressjs.com. With it, we can easily create a server running without having to create the boilerplate from scratch. There are at least two big things that separates Koa and Express, which are:
Sending responses
In Express, we send responses by using `res.send("Hello world")`
, in which the `res`
is Express’ `Response`
object. In Koa, we use `ctx.body = "Hello world"`
, in which the `ctx`
is Koa’s context. Find more information about Koa’s context in this API document.
Middlewares
In Express, the middleware is synchronous, whereas in Koa, the middleware is asynchronous (returns a `Promise`
). Take this example:
// Expressapp.use((req, res, next) => { console.log('LOGGED'); next();});
// Koaapp.use((ctx, next) => { console.log('LOGGED'); // `next` returns a `Promise` return next();});
Logging and middlewares
Logging is an important part when we are running a service. Without logging, we can’t really monitor how a service is doing, how many requests are going in, how many errors happened, and so on. In this section onwards, we will only do examples in Koa.
The goal that I wanted to achieve was to log the request and response body with morgan. So, before I got to using it, I broke it down into several steps while trying to understand how middlewares work. The incoming request will be made with this script:
const axios = require('axios');
async function run() { const response = await axios('http://localhost:3000', { method: 'post', data: { userId: '123' } });
console.info(response.data);}
run();
Good old console.log
Before using `morgan`
, I wanted to know first how middlewares behave. In the example below, I tried logging the response body before and after the `await next()`
is called. Recall that the Koa middleware returns a `Promise`
, so it makes sense to make it an `async`
function because a function that is prefixed by `async`
automatically returns a `Promise`
.
const Koa = require('koa');const { bodyParser } = require('@koa/bodyparser');
const app = new Koa();
app.use(bodyParser());
app.use(async (ctx, next) => { console.log('hello', ctx.request.body, ctx.body); await next(); console.log('hello', ctx.request.body, ctx.body);});
app.use((ctx) => { ctx.status = 200; ctx.body = { message: ctx.request.body };});
app.listen(3000);
In the example above, the result will be as follows:
hello { userId: '123' } undefinedhello { userId: '123' } { message: { userId: '123' } }
As we can see from the result above, the first `console.log`
will not be able to log the response body, because when it is called, `ctx.body`
hasn’t been set yet. Conversely, this means the second `console.log`
is executed only after all middlewares are done.
Now that normal logging works, let’s try integrating `morgan`
into it.
With morgan
There are two ways to do it. First things first, we have to import the `morgan`
package first and set up the tokens.
const morgan = require('morgan');
morgan.token('requestBody', (req) => { return JSON.stringify(req.body);});
morgan.token('responseBody', (_, res) => { return JSON.stringify(res.body);});
Now, we can either put it before the `next()`
call or after it. Let’s explore both ways.
Before next()
We want to jump a bit to how `morgan`
works. Take a look at this snippet of morgan’s source code.
if (immediate) { // immediate log logRequest();} else { // record response start onHeaders(res, recordStartTime); // log when response finished
onFinished(res, logRequest);}
next();
What the above means is that we have an `immediate`
option that we can pass when we initialize `morgan`
middleware. When that is passed, the request is logged immediately, but if not, then the request will be logged only after the response is finished. The `onFinished`
function is based on the on-finished package.
By using the above understanding, we can do the following:
const logger = morgan(':date :requestBody :responseBody');
app.use((ctx, next) => { logger(ctx.request, ctx.response, () => {});
return next();});
When the request is made, it will log the following.
Sun, 13 Apr 2025 00:57:29 GMT {"userId":"123"} {"message":{"userId":"123"}}
After next()
Similarly, we can also do this:
const logger = morgan(':date :requestBody :responseBody', { immediate: true});
app.use(async (ctx, next) => { await next();
logger(ctx.request, ctx.response, () => {});});
When we make the request, it will provide the same result:
Sun, 13 Apr 2025 00:57:29 GMT {"userId":"123"} {"message":{"userId":"123"}}
Cleaning things up
Based on the attempts before, we can clean things up a little bit to this:
const logger = morgan(':date :requestBody :responseBody');
app.use((ctx, next) => { return new Promise((resolve) => { logger(ctx.request, ctx.response, resolve); }).then(next);});
This allows the middleware to still return a `Promise`
while allowing the `morgan`
logger to chain with the `next`
call naturally. The request will still be correctly logged.
Sun, 13 Apr 2025 01:16:50 GMT {"userId":"123"} {"message":{"userId":"123"}}
I happened to also find a flaw in the `@types/morgan`
package.
type Handler< Request extends http.IncomingMessage, Response extends http.ServerResponse> = (req: Request, res: Response, callback: (err?: Error) => void) => void;
If we check the `morgan`
implementation, the third parameter `next`
is always called without an argument, so the `callback`
here will always be called with an `undefined`
parameter. Also, the name should be `next`
instead of `callback`
, I suppose.
Recap
Okay, let’s recap what we have discussed so far:
`morgan`
internally uses the`on-finished`
package, allowing it to listen to when a response’s write buffer has been closed or not.- Koa middlewares return
`Promise`
. `await next()`
can be used to “defer” the remaining line of code in the middleware to later stages.- The package
`@types/morgan`
returns an invalid type for the`Handler`
function.
That’s all for now. Hopefully, this post is useful. Thank you for reading and take care!