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.

ELab Team
ELab Team
ELab Team
How to Build a Minimal Node.js Server and Master Koa’s Middleware Architecture

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/about

Inspecting 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)

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Strategy PatternJavaScriptmiddlewareNode.jsHTTP serverKoa
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.