◆ next() を実行しない限り次のミドルウェアは実行されない
  ◆ for-of みたいな処理は next を呼びだすことで次を実行
◆ next() を await で待機するとそれ以降のミドルウェアの処理が全部終わったあとで処理できる
◆ next に引数は渡せないけど return は next() の返り値になる

最近 framework は koa にしてる

Node.js の有名どころの Framework は express/koa/hapi があります
koa は express と近くて express のメジャーアップデートにしてもいいと思えるくらいに似てます
とは言え 内部の設計や方針も違って互換性もそこまでないので別の Framework になってます

express も koa もミドルウェアという考え方で リクエストに対して設定したミドルウェアをいくつも通していくというものになります
この辺はなれないとイメージが付きづらいです
それに対して hapi は設定ファイルを書くような感じでオブジェクトを渡せばいいので わかりやすいです

そういうわけでこれまでは hapi がメインだったのですが koa などに比べると標準で入ってる機能が多い分 そこまで速くないですし 設定ファイル形式でオブジェクトを書くにしてもどこに何を書くというのがけっこう複雑で毎回ドキュメント見てるとあんまり便利でもない気がしました
実際 こうなってれば便利なのにと思うことでも対応してないのがいくつもあって issues 見ても作ってる人がお堅い感じでそういう方針じゃないとかこうあるべきだ みたいな感じでサッとクローズしてしまってるのが多くて 利便性をそこまで追求してないように見えます
それならそれでいいのですが なんか不便だな感じる事が多いので それならいっそ koa にしてみようかなと 最近 koa を使うようになりました

koa は基本機能は少なく凄くシンプルなので 自分でカスタマイズするのにも向いています
koa を使ってこなかった理由が ミドルウェアのわかりづらさというか使いやすそうに思えない部分だったので そこをちゃんと理解できれば特に困ることはなさそうです
そこまで使ってないですが express よりもシンプルな作りになってるので ミドルウェアについても koa のほうがわかりやすいと思います

koa のミドルウェア

koa では hapi のようにルートごとに設定するのとは違い 全体に対してミドルウェアという処理と順に通していきます
ミドルウェアにはルートと言う概念はないので標準ではルーティング機能もついてません
ミドルウェアのひとつとして パスとリクエストメソッドが目的のものならこういう処理をするというミドルウェアを自分で作ることでルーティング可能です

app.use((ctx, next) => {
if(ctx.request.method === "GET" && ctx.request.path === "/"){
ctx.body = "Hello"
}
next()
})

公式のパッケージの koajs/route では こういう感じでルーティングを行います

const route = require("koa-route")
app.use(route.get("/", something1))
app.use(route.get("/foo", something2))

上の自分で作ったようなものを中でやってるだけなので ひとつひとつのルートをミドルウェアとして use しています

一応 公式ではないルーターライブラリ(人気なのにメンテされてないとかで今はフォークされて公式が管理)もあって こっちはルーティング機能だけでまとめてひとつのミドルウェアにして登録する形になってます
koajs/koa-router

ミドルウェアはミドルと言いつつも中間に位置するとは限りません
ミドルウェアが引数に受け取る next 関数を呼び出さない限りは次は登録していても実行されません
なので ルーティングする前に 特定のリクエストはエラーを返すこともできます
末尾が / なら / なしへリダイレクトなども簡単です
next の返り値を await すれば次の処理が終わったあとに処理を行うことができるので ルートごとの処理が終わったあとに何かを行うこともできます
レスポンスを返す直前にログを保存するなども簡単です
hapi だとルートごとの処理は楽でも こういう複数ルートに関わる処理をするのは面倒なんですよね

ミドルウェアの詳細

Koa の内部の処理を見てみます
app.use で登録したミドルウェアは単純に配列に追加されます

this.middleware.push(fn);

その他エラーのチェックなどはありますが 実際のやってることは配列に push だけです
middleware はコンストラクタで空の配列がセットされるので単純に use で渡した引数が配列に保存されてるだけです

サーバからリクエストが来たときのコールバック関数は callback 関数で作っています
listen の処理でこういう感じで設定されます

    const server = http.createServer(this.callback());
return server.listen(...args);

  callback() {
const fn = compose(this.middleware);

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;
}

handleRequest 関数が Node.js が直接呼び出すものです
リクエストのたびに ctx を作って handleRequest メソッドにミドルウェアと一緒に渡しています
ctx はミドルウェア関数の引数として受け取るあれです
ミドルウェアの一覧はリクエストごとに変わらないので 最初に一度 compose で関数にしています

handleRequest 関数内で呼び出している this.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);
}

ミドルウェアの配列を compose して作った関数を呼び出して 終われば respond 関数を実行してエラーが出たら ctx.onerror を呼び出してます
どっちも最終処理で ctx の値に応じて レスポンスを返す処理です
header を設定したり res.end を実行したりそういう感じです
長いのでここは略します

onFinished でも onerror を呼び出してますが onerror の中で最初の引数の err が空なら何もしないので res でエラーが起きたときだけの処理のようです
onFinished じゃなくて onerror で受け取ればいいのにと思いましたがなんか理由があるのでしょう

fnMiddleware は compose で作った関数なので compose をみてます
koajs/compose

関数を 1 つエクスポートしてるだけの単純なものです

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

fnMiddleware はここで reutrn している関数です
実行されると dispatch(0) で 0 番目のミドルウェアが最後の try のところで実行されます
Promise.resolve とかあってちょっと複雑ですが dispatch を async 関数にすればこの辺いらなくなるのに と思います

実行されるとき 1 つ目の引数は context で これは ctx のことです
2 つ目の引数は dispatch になるのですが 引数にミドルウェアのインデックスになる i が +1 された値を bind したものになります
呼び出せばミドルウェア配列で次のミドルウェアが実行されます
これが next になっていて また dispatch はどれも Promise を返すので await すれば次のミドルウェアの実行を待機できます
配列の最後まで行くと Promise.resolve() を返すので その next は特に何もせず終了になります
最後でも気にせず next を実行して大丈夫です

こういう仕組みなので 一番最初に登録したミドルウェアはリクエストが来て最初に処理を実行できて next のあと つまり以降のミドルウェアすべての処理のあとの最後 にも処理ができます
next で次のミドルウェアを呼び出したあとに もうすることがないのなら async 関数にせず普通に next を呼び出せば良いです

ミドルウェア内の compose

この compose ですが Koa の本体だけでなくミドルウェア内部でも使われています
koajs/koa-router だとミドルウェアを出力する routes メソッドの最後がこうなっています

return compose(layerChain)(ctx, next);

2 つ目の引数に next を入れることで親の next を続けることができます
この compose の配列の最後まで行ったときの next が引数で渡した親の next を呼び出すことになります
ミドルウェアチェーンのサブグループみたいなものが作れるわけです

ところで このルーターミドルウェアでは中で compose を使ってミドルウェアのチェーンで処理しているので 優先順位は先に設定された順になります

/:part/bar
/foo/bar

など複数にマッチするときは 先に登録したほうが実行されます
また next を呼び出せば次にマッチしたものが呼び出せます

この辺は hapi に比べると好きなところだったりします
内部ルールによる詳細度に応じて自動で優先度決めるなんておせっかいでしかないです
CSS の詳細度と同じく迷惑なだけなやつです
また 幅広くマッチさせて条件によっては次のルートに移行できるのもけっこう便利な機能です
単純にパスとメソッドだけじゃなくてクエリだったりサーバのデータによって変えたいこともありますからね

それと複数ルートにマッチした場合の処理を見てみると 少し気になる部分がありました

    var matchedLayers = matched.pathAndMethod
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name;
}

マッチしたものの配列の最後を mostSpecificLayer として _matchedRoute に保存しています
なぜか最後のものです
たいていマッチした場合は最初のほうから見ていって next が呼び出されないことが多いと思います
なので最後までいくことはあまりないと思うのにそれのルートや名前を保存してるのってなんなのでしょうね
どう使われているのか 探してみたのですが特に使われてないようですし

ミドルウェア間のデータ受け渡し

next に引数を渡したい気もしますが 渡しても dispatch の 2 つ目移行の引数になるので処理されず捨てられます
次のミドルウェアの 3 つ目移行の引数にはなってくれません
ですが 逆で next で実行したミドルウェアが return で返した値は next の返り値を await することで取得できます

しかし これに頼ると次にこのミドルウェアを実行するというのが守られないといけなくなります
使うユーザが間に別のをいれてしまうと 動かなくなるようなものなので それを前提にしたミドルウェアは作らないほうが良いのじゃないかと思ってます
引数は渡せないのに返り値だけ受け取れるという動き自体 意図したものじゃなく偶然実装上そうなってるって感じがして 将来的変わらない保証もない気がします
ただ ミドルウェアによっては return に値を入れてるのもあるので 一切使われてないわけでもなさそうです
ライブラリのミドルウェアじゃなくて自分が作るアプリケーション用のミドルウェアならありなのかもしれません