Based on Nodejs platform web framework-Express VS Koa

Posted Jun 6, 20203 min read

Express and Koa are both web frameworks based on the Nodejs platform, and are currently more common frameworks for rapid development of web services, and both are based on middleware to process client requests, so what is the difference between the two?
To put it simply, "Express is a linear type and Koa is an onion model".(Shouting slogans!!!)
Let's take a look at the following sample code first:

//for express example
const express = require('express');

const app = express();

function cb1(req, res, next) {
    console.log('>>>>>>cb1');
    next();
    console.log('<<<<<<cb1');
}

function cb2(req, res, next) {
    console.log('>>>cb2<<<');
    res.send('hello world');
}

app.use('/', [cb1, cb2]);
app.listen(3000);

//for koa2 example
const koa = require('koa2');

const app = koa();

function cb1(ctx, next) {
    console.log('>>>>>>cb1');
    next();
    console.log('<<<<<<cb1');
}

function cb2(ctx, next) {
    console.log('>>>cb2<<<');
    ctx.body ='hello world';
}

app.use(cb1);
app.use(cb2);
app.listen(3000);

The output of the above two codes is:

>>>>>>cb1
>>>cb2<<<
<<<<<<cb1

Therefore, when middleware is a synchronous function, there is no difference between the two in terms of execution results.
Let's take a look at the following sample code again:

//for express example
const express = require('express');

const app = express();

async function cb1(req, res, next) {
    console.log('>>>>>>cb1');
    await next();
    console.log('<<<<<<cb1');
}

async function cb2(req, res, next) {
    return new Promise((resolve) => {
        setTimeout(resolve, 500);
    }).then(() => {
        console.log('>>>cb2<<<');
        res.send('hello world');
    });
}

app.use('/', [cb1, cb2]);
app.listen(3000);

//for koa2 example
const koa = require('koa2');

const app = new koa();

async function cb1(ctx, next) {
    console.log('>>>>>>cb1');
    await next();
    console.log('<<<<<<cb1');
}

async function cb2(ctx, next) {
    return new Promise((resolve) => {
        setTimeout(resolve, 500);
    }).then(() => {
        console.log('>>>cb2<<<');
        ctx.body ='hello world';
    });
}

app.use(cb1);
app.use(cb2);
app.listen(3000);

The output of express-example is:

>>>>>>cb1
>>>>>>cb1
>>>cb2>>>

The output of koa2-example is:

>>>>>>cb1
>>>cb2<<<
<<<<<<cb1

As can be seen from the above example, when middleware is an asynchronous function, the execution flow of Express and Koa is different. The return result of Express is not the result we imagined. What caused the difference in behavior? Next, let us briefly analyze the source code fragments of the middleware part of Express and Koa.
In Express, the logic code to execute middleware is mainly located in the lib/router/route.js and lib/router.layer.js files:

//route.js
Route.prototype.dispatch = function dispatch(req, res, done) {
  var idx = 0;
  var stack = this.stack;
  if(stack.length === 0) {
    return done();
  }

  var method = req.method.toLowerCase();
  if(method ==='head' && !this.methods['head']) {
    method ='get';
  }

  req.route = this;

  next();

  function next(err) {
    //signal to exit route
    if(err && err ==='route') {
      return done();
    }

    //signal to exit router
    if(err && err ==='router') {
      return done(err)
    }

    var layer = stack[idx++];
    if(!layer) {
      return done(err);
    }

    if(layer.method && layer.method !== method) {
      return next(err);
    }

    if(err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};


//layer.js
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
  var fn = this.handle;

  if(fn.length !== 4) {
    //not a standard error handler
    return next(error);
  }

  try {
    fn(error, req, res, next);
  } catch(err) {
    next(err);
  }
};

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if(fn.length> 3) {
    //not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch(err) {
    next(err);
  }
};

In Koa2, the logic code to execute middleware is mainly located in the koa-compose/index.js file:

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, function next() {
          return dispatch(i + 1)
        }))
      } catch(err) {
        return Promise.reject(err)
      }
    }
  }
}

From the above, the next parameter of middleware in Express is an ordinary function object, and the next parameter of middleware in Koa is a promise object. So when we mount an asynchronous middleware, Express cannot be like Koa, use await in middleware to wait for the next middleware execution to complete, and then execute the subsequent logic of the current middleware. This is the basic reason why "Express is a linear type and Koa is an onion model".
The above is my understanding of Express and Koa framework, I hope to help you. If there is a mistake in the above content, please welcome to correct me, thank you~