How to Build a Minimal Node.js Server and Master Koa’s Middleware Architecture
This article walks through creating a simple native Node.js HTTP server, applying the Strategy pattern to decouple routes, enforcing the DRY principle with Promises, and then dives deep into Koa’s core source—including its constructor, use, listen, compose, and router modules—to reveal how the framework orchestrates middleware and request handling.
1 Native Implementation
1.1 Start a Service
Node can start a service with just a few lines of code; the example creates an HTTP server that listens on port 8888 and responds with "Hello World!".
const http = require('http')
const handler = (req, res) => {
res.end('Hello World!')
}
http
.createServer(handler)
.listen(8888, () => {
console.log('listening 127.0.0.1:8888')
})Visiting 127.0.0.1:8888 returns the string "Hello World!". Using curl with different methods or paths still returns the same response.
curl 127.0.0.1:8888
curl -X POST http://127.0.0.1:8888
curl 127.0.0.1:8888/aboutInspecting the req object shows that method and url can be used to differentiate routes, leading to a switch‑based handler:
const http = require('http')
const handler = (req, res) => {
let resData = '404 NOT FOUND!'
const { method, path } = req
switch (path) {
case '/':
if (method === 'get') {
resData = 'Hello World!'
} else if (method === 'post') {
resData = 'Post Method!'
}
break
case '/about':
resData = 'Hello About!'
}
res.end = resData
}
http
.createServer(handler)
.listen(8888, () => {
console.log('listening 127.0.0.1:8888')
})As the number of routes grows, the handler becomes unwieldy, prompting a decoupling of path and method using the Strategy pattern.
1.2 Strategy Pattern Decoupling
The Strategy pattern cleanly separates route registration from request handling.
const http = require('http')
class Application {
constructor() {
// collect route‑method callbacks
this.$handlers = new Map()
}
// register handler
register(method, path, handler) {
let pathInfo = null
if (this.$handlers.has(path)) {
pathInfo = this.$handlers.get(path)
} else {
pathInfo = new Map()
this.$handlers.set(path, pathInfo)
}
// register callback
pathInfo.set(method, handler)
}
use() {
return (request, response) => {
const { url: path, method } = request
this.$handlers.has(path) && this.$handlers.get(path).has(method)
? this.$handlers.get(path).get(method)(request, response)
: response.end('404 NOT FOUND!')
}
}
}
const app = new Application()
app.register('GET', '/', (req, res) => {
res.end('Hello World!')
})
app.register('GET', '/about', (req, res) => {
res.end('Hello About!')
})
app.register('POST', '/', (req, res) => {
res.end('Post Method!')
})
http
.createServer(app.use())
.listen(8888, () => {
console.log('listening 127.0.0.1:8888')
})1.3 DRY Principle
If the method string is written in lower case, it will not match the upper‑case Http.Request.method, causing a 404.
Adding a timestamp to every handler forces modifications in each register call, violating DRY.
Using Promise we can chain multiple handlers while preserving order.
const http = require('http')
class Application {
constructor() {
this.$handlers = new Map()
this.get = this.register.bind(this, 'GET')
this.post = this.register.bind(this, 'POST')
}
register(method, path, ...handlers) {
let pathInfo = null
if (this.$handlers.has(path)) {
pathInfo = this.$handlers.get(path)
} else {
pathInfo = new Map()
this.$handlers.set(path, pathInfo)
}
// store an array of handlers
pathInfo.set(method, handlers)
}
use() {
return (request, response) => {
const { url: path, method } = request
if (this.$handlers.has(path) && this.$handlers.get(path).has(method)) {
const _handlers = this.$handlers.get(path).get(method)
_handlers.reduce((pre, _handler) => {
return pre.then(() => {
return new Promise((resolve, reject) => {
_handler.call({}, request, response, () => {
resolve()
})
})
})
}, Promise.resolve())
} else {
response.end('404 NOT FOUND!')
}
}
}
}
const app = new Application()
const addTimestamp = (req, res, next) => {
setTimeout(() => {
this.timestamp = Date.now()
next()
}, 3000)
}
app.get('/', addTimestamp, (req, res) => {
res.end('Hello World!' + this.timestamp)
})
app.get('/about', addTimestamp, (req, res) => {
res.end('Hello About!' + this.timestamp)
})
app.post('/', addTimestamp, (req, res) => {
res.end('Post Method!' + this.timestamp)
})
http
.createServer(app.use())
.listen(8888, () => {
console.log('listening 127.0.0.1:8888')
})1.4 Reduce Cognitive Load
Expose a simple next function so users can advance to the next handler without manually chaining Promises.
class Application {
// ...
use() {
return (request, response) => {
const { url: path, method } = request
if (this.$handlers.has(path) && this.$handlers.get(path).has(method)) {
const _handlers = this.$handlers.get(path).get(method)
_handlers.reduce((pre, _handler) => {
return pre.then(() => {
return new Promise(resolve => {
// expose next to the user
_handler.call({}, request, response, () => {
resolve()
})
})
})
}, Promise.resolve())
} else {
response.end('404 NOT FOUND!')
}
}
}
}
const addTimestamp = (req, res, next) => {
setTimeout(() => {
this.timestamp = new Date()
next()
}, 3000)
}2 Koa Core Source Analysis
After building a simple middleware framework, we examine Koa’s core files: application.js, request.js, response.js, and context.js. Koa creates an instance, initializes a middleware array, and inherits context, request, and response via Object.create.
2.1 constructor
constructor(options) {
super();
// instance configuration
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
// store middleware
this.middleware = [];
// inherit objects
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}The two most important instance methods are use (register middleware) and listen (start the HTTP server).
2.2 use
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// support generator functions (deprecated in v3)
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3.');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
// enable chaining
return this;
}2.3 listen
listen(...args) {
debug('listen');
// create server
const server = http.createServer(this.callback());
// forward arguments to http.Server.listen
return server.listen(...args);
}2.4 callback
Compose middleware into a Promise chain.
Register a default error listener if none is provided.
Create the request handler that builds ctx via createContext and then calls handleRequest.
callback() {
// koa-compose core
const fn = compose(this.middleware);
// ensure error handling
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}2.5 createContext
createContext(req, res) {
const context = Object.create(this.context);
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;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}2.6 handleRequest
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}3 Koa-compose
Koa‑compose validates the middleware array and returns a function that dispatches each middleware in order, enforcing the onion model and preventing multiple next() calls.
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!');
}
return function (context, next) {
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);
}
}
};
}4 Koa-router
Koa‑router adds route handling with parameters. It registers routes via router.get, router.post, etc., and builds a layer that matches paths using regular expressions.
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('/', async ctx => {
ctx.body = 'Hello World!'
})
router.get('/:userName', async ctx => {
ctx.body = `Hello ${ctx.params.userName}!`
})
app
.use(router.routes())
.use(router.allowedMethods())
.listen(8888)router.register (core)
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
const router = this;
const stack = this.stack;
if (Array.isArray(path)) {
for (let i = 0; i < path.length; i++) {
const curPath = path[i];
router.register.call(router, curPath, methods, middleware, opts);
}
return this;
}
const route = new Layer(path, methods, middleware, {
end: opts.end === false ? opts.end : true,
name: opts.name,
sensitive: opts.sensitive || this.opts.sensitive || false,
strict: opts.strict || this.opts.strict || false,
prefix: opts.prefix || this.opts.prefix || "",
ignoreCaptures: opts.ignoreCaptures
});
if (this.opts.prefix) {
route.setPrefix(this.opts.prefix);
}
for (let i = 0; i < Object.keys(this.params).length; i++) {
const param = Object.keys(this.params)[i];
route.param(param, this.params[param]);
}
stack.push(route);
debug('defined route %s %s', route.methods, route.path);
return route;
};5 Related Documents
koa onion model
Strategy pattern (Wikipedia)
JavaScript design patterns – Strategy (Rob Dodson)
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
