Nuxt で nodemon を使わずにサーバー処理をホットリロードする

Nuxt で create-nuxt-app を使って express プロジェクトを作ると、server ディレクトリ以下を監視し、変更があった場合には nodemon で自動的にサーバーを再起動してくれるように設定されています。

```json:package.json
  "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
```

一見問題なさそうなのですが実際に使ってみると、再起動する度にクライアント用コードの再コンパイルが実行されるため、変更の反映に時間がかかりすぎ気持ちよくコーディングができません。

そこで「[Don’t use nodemon, there are better ways!](https://codeburst.io/dont-use-nodemon-there-are-better-ways-fc016b50b45e)」に書かれている方法を使って nodemon を使わないホットリロードを実現しようと思います。

なお、この記事を書いた時点での create-nuxt-app のバージョンは、2.1.1 です。

## nodemon の削除

まず、nodemon は不要になるので、取り除いてしまいます。

```bash
npm uninstall nodemon --save-dev
```

```diff:package.json
-    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
+    "dev": "cross-env NODE_ENV=development node server/index.js",
```

このあと、本来は chokidar と esm をインストールすべきなのですが、nuxt 2.0.0 時点では依存関係に含まれているので、一旦インストールしません。[^deps]

[^deps]: 依存関係を明示的に加えた方が安全ではあります。

## サーバ処理のエントリポイントの追加

サーバ処理以外は、Nuxt 側に移譲する必要があるため、サーバ処理にディスパッチするためのエントリポイントを用意します。ここでは /api 以下のパスにアクセスした場合 server/api 以下に置かれたミドルウェアをディスパッチするように設定します。

```diff:server/index.js
+ const esm = require('esm')(module)
...
+  // Dispatch api requests
+  app.use('/api', function (req, res, next) {
+    if (/(^|\/)\.+(\/|$)/.test(req.path)) {
+      return next()
+    }
+
+    let module;
+    try {
+      module = esm('./api' + req.path + (req.path.endsWith('/') ? 'index' : '') + '.js')
+    } catch(e) {
+      return next()
+    }
+
+    const middleware = module[req.method.toLowerCase()] || module.default;
+    if (middleware) {
+      middleware(req, res, next)
+    } else {
+      return next()
+    }
+  })

  // Give nuxt middleware to express
  app.use(nuxt.render)
```

## ホットリロード機能の追加

次に server/api 以下のミドルウェアが変更された場合に require のキャッシュをクリアする処理を加えます。この処理は、開発中しか使用しないので、config.dev ブロック中に置きます。

```diff:server/index.js
  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()

+    // Monitor file changes
+    const chokidar = require('chokidar')
+    chokidar.watch('./server/api').on('all', function() {
+      Object.keys(esm.cache).forEach(function(id) {
+        if (/[\/\\]server[\/\\]api[\/\\]/.test(id)) {
+          delete esm.cache[id]
+        }
+      })
+    })
  }
```
## 完成版

最終的には次のようになりました。server/api 以下にミドルウェアを配置すると、/api 以下のパスから呼び出すことが可能です（例えば、/api/test にアクセスすると server/api/test.js の処理が呼び出されます）。

```js:server/index.js

const express = require('express')
const consola = require('consola')
const esm = require('esm')(module)
const { Nuxt, Builder } = require('nuxt')
const app = express()
const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || 3000

app.set('port', port)

// Import and Set Nuxt.js options
let config = require('../nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')

async function start() {
  // Init Nuxt.js
  const nuxt = new Nuxt(config)

  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()

    // Monitor file changes
    const chokidar = require('chokidar')
    chokidar.watch('./server/api').on('all', function() {
      Object.keys(esm.cache).forEach(function(id) {
        if (/[\/\\]server[\/\\]api[\/\\]/.test(id)) {
          delete esm.cache[id]
        }
      })
    })
 }

  // Dispatch api requests
  app.use('/api', function (req, res, next) {
    if (/(^|\/)\.+(\/|$)/.test(req.path)) {
      return next()
    }

    let module;
    try {
      module = esm('./api' + req.path + (req.path.endsWith('/') ? 'index' : '') + '.js')
    } catch(e) {
      return next()
    }

    const middleware = module[req.method.toLowerCase()] || module.default;
    if (middleware) {
      middleware(req, res, next)
    } else {
      return next()
    }
  })

  // Give nuxt middleware to express
  app.use(nuxt.render)

  // Listen the server
  app.listen(port, host)
  consola.ready({
    message: `Server listening on http://${host}:${port}`,
    badge: true
  })
}
start()
```