Middleware
When it comes to Node.js web frameworks, the most fascinating design is undoubtedly "middleware." Whether it's Express or Koa (Egg is a framework built on top of Koa), middleware is a major feature. Koa in particular — countless developers adopted it specifically for its "onion model middleware." Let's use the source code to explain the principles of Koa's onion middleware:
"use strict";
/**
* Expose compositor.
*/
module.exports = compose;
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose(middleware) {
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!");
for (const fn of middleware) {
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!");
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
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();
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}There's very little code — the core is barely a dozen lines. Let me add comments to explain how it works:
function compose(middleware) {
// Throw error if input is not an array
if (!Array.isArray(middleware)) throw new TypeError("Middleware stack must be an array!")
// Throw error if any element is not a function
for (const fn of middleware) {
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!")
}
return function(context, next) {
// index starts at -1
let index = -1
// Execute the first middleware by default
return dispatch(0)
function dispatch(i) {
// Throw error if next() is called multiple times
if (i <= index) return Promise.reject(new Error("next() called multiple times"))
// Set current middleware index
index = i
let fn = middleware[i]
// When i equals the last index of the middleware array, fn is undefined (set to next)
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
// This is the core code. It returns a Promise.
// When execution reaches a middleware without next(), the call stack unwinds,
// resuming upstream behavior from the last middleware — forming the onion model.
// If you're not familiar with stacks, brush up on stack data structure concepts.
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}The compose function itself returns a Promise. Through each middleware's await next(), execution flow is controlled. next() is essentially the next middleware function, so code after await next() only executes after the downstream middleware completes. Interestingly, redux-thunk borrows this same design pattern. React developers or anyone interested can check it out.
Conclusion
What matters in frameworks and libraries isn't fancy code — it's design philosophy. Once you understand the design thinking, you can write those impressive-sounding technologies yourself.
