Deep Dive into Koa: Source Code Analysis
Introduction
This article provides a comprehensive, in-depth analysis of the core source code of the popular Koa framework from the ground up. It is suitable for developers who are already proficient in using the Koa framework.
Core Mechanism
Now, let’s start from the beginning to see what Koa actually does internally?
// usage
const app = new Koa()// source code
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}This is where everything begins. The instance of the Koa class is born, inheriting from Events, which indicates that its subclasses have the ability to handle asynchronous events. However, how Koa handles them is still unknown for now, so let’s mark it with a question mark. During the instantiation process, we can see that three objects are initialized as instance properties: context, request, and response, along with the familiar array middleware that stores all global middleware.
Koa Class Diagram

// usage
app.use(async (ctx, next) => {
// middleware function
});When the use method is called, after confirming it’s an async function, this function will be appended to the middleware array through a push operation:
use(fn) {
// type checking...
this.middleware.push(fn);
return this;
}At this point, we already have the processing operations, but koa hasn’t really started running yet.
app.listen(3000);We can say that when the http server is started, koa can truly begin processing our http requests. So what exactly does this concise call do behind the scenes?
const server = http.createServer(this.callback());
return server.listen(...args);koa uses Node’s native http package to create an http service. All the secrets are hidden within the callback() method.
// source code
const fn = compose(this.middleware);
...
return fn(ctx).then(handleResponse).catch(onerror);koa itself depends on the koa-compose module. From the way koa uses fn, it appears that middleware should be encapsulated into an object called fn, which returns a Promise by passing in the context object.
Now, let’s dive deep into the koa-compose module to see what it does to our middleware array.
// source code
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve() // recursion ends
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1) // recursive call to dispatch
}))
} catch (err) {
return Promise.reject(err)
}
}
}The closure inside looks very familiar, doesn’t it? Let’s compare again:
// usage
app.use(async (ctx, next) => {
// middleware function
});Here, Promise.resolve(fn(..)) helps us execute the async middleware functions, and the next function explains why Koa’s middleware execution is recursive. It recursively calls the dispatch function to traverse the array. Meanwhile, all middleware functions share the same ctx. Let’s review the external part again:
const handleRequest = (req, res) => {
res.statusCode = 404;
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fn(ctx).then(handleResponse).catch(onerror);
};
return handleRequest;The context uses the req and res from Node’s native http listener callback function for further encapsulation. This means that for each http request, koa creates a context and shares it among all global middleware. After all middleware have finished executing, the final data to be returned is unified and passed back to res for response. Therefore, in each middleware, we can obtain the data we need from req through ctx for processing, and finally ctx returns the response body to the native res.
Each request has a unique context object, where all request and response related information is unified.
The createContext method further encapsulates req and res:
// source code
const context = Object.create(this.context); // Create an object that has context's prototype methods, following the same pattern
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;Following the principle of one request per context, context must exist as a temporary object, with everything encapsulated in one object. Therefore, the three properties app, req, and res were created. But you might wonder, why are app, req, and res also encapsulated in both request and response?
By making them share the same app, req, res, and ctx, it enables the transfer of processing responsibilities. Users accessing from outside only need one ctx to get all the data and methods provided by koa. Then koa continues to divide these responsibilities further, such as request being used to further encapsulate req, and response being used to further encapsulate res. This disperses responsibilities, reduces coupling, and sharing all resources makes the entire context highly cohesive, allowing internal elements to access each other.
// source code
context.state = {};As we can see, state is specifically responsible for storing individual request states as an empty object, allowing users to manage its contents according to their needs.
// source code
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);When this large ctx is created, it immediately mounts an error listener. However, the onerror method is not found in createContext. It should belong to one of the modules’ prototype methods. After some searching, we find it located in the context.js file, which defines all the prototype methods that the default context object possesses.
// application.js
// handleRequest()
const onerror = err => ctx.onerror(err);
onFinished(res, onerror);
return fn(ctx).then(handleResponse).catch(onerror);If exceptions are thrown during the execution of async functions and the exceptions are not captured internally, koa will uniformly use the catch statement of Promise for error handling. Represented in a diagram, it would look like this:
Koa Request Processing Flow Chart

By now, we should be able to resolve the question we had at the beginning of this article: why inherit from Events. It’s actually to use Node’s built-in event listeners to listen to certain events, such as currently known error, to decouple the functionality of error handling.
Global error handling function:
// source code
// application.js
onerror(err) {
assert(err instanceof Error, `non-error thrown: ${err}`);
if (404 == err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}The internal core processing flow of Koa has been sorted out. Now let’s explore the internal modules.
Request Module
A large number of getter and setter methods fully extract useful request-related properties provided by Node’s http, along with some necessary helper functions that won’t be elaborated here. Almost all properties under ctx.req in the API documentation are provided here. It’s worth mentioning the this in this section - the troublesome this. This block of code uses a large number of this.req ways to access Node’s native HTTP request information (since we assign Node’s req to context). When using it externally in koa, we access it through the form ctx.req.propertyName… Yes, I think you’ve noticed too. We need to bind our this to point to ctx.req, so in the context.js module, delegation is used to properly bind the this. The assignment code mentioned earlier that enables these modules to access each other actually reduces this troubles to some extent.
// See delegates package for details
// https://github.com/tj/node-delegates
// Koa context.js
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.access('query')
.access('path')
.access('url')
.getter('origin')
// Internal core implementation of delegates, rebind this via apply
proto[name] = function(){
return this[target][name].apply(this[target], arguments);
};Response Module
The internal structure of response.js is similar to request.js. It’s worth mentioning the writable getter - if the request has received a response, it returns true. The value it reads is this.res.finished. In many error messages, we often encounter errors about attempting to write response headers header repeatedly. Even the source of this error comes from the on-finished package, which can execute a callback to save the request status to res when the response ends.
Response Helper Functions
Koa has a unified response function located at the end of application.js, specifically responsible for handling ctx.body and ctx.status for request responses.
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);It’s worth noting that the returned body supports Buffer, string, streams, and most commonly json.
Views