Fusic Tech Blog

Fusicエンジニアによる技術ブログ

NestJSでHot Module Replacementを使った開発環境構築
2024/03/25

NestJSでHot Module Replacementを使った開発環境構築

NestJSでは nest new コマンドで作成したプロジェクトはデフォルトで npm run start:dev コマンドが設定されています。

しかし、このコマンドはnodemon を使用しファイルの変更を監視して変更があればサーバーを再起動させるというものです。

これだとファイルを変更するたびにサーバーの再起動、つまり全ファイルのTypeScriptコンパイルが毎回走ってしまい、再起動自体に時間がかかり効率も悪いですし、マシンに負荷がかかってしまいます。

この問題を解決するためにwebpack を使ってHot Module Replacement(HMR) を有効にする、つまり変更があった部分(モジュール)のみ更新するようにすることでコンパイル時間の短縮や負荷を軽減することができます。

NestJSではドキュメントに設定方法が記載されていますが、それに従って設定していきます。

環境

  • Node.js: 12.6.0
  • @nestjs/cli: 6.5.0

パッケージのインストール

webpackなどHMRを有効にするためのパッケージをインストールします。

ドキュメントでインストールしているパッケージに加え、webpackの型定義ファイル(@types/webpack-env )もインストールしています。

$ npm install --only=dev webpack webpack-cli webpack-node-externals ts-loader @types/webpack-env

webpack設定

// webpack.config.js
const webpack = require('webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  entry: ['webpack/hot/poll?100', './src/main.ts'],
  watch: true,
  target: 'node',
  externals: [
    nodeExternals({
      whitelist: ['webpack/hot/poll?100'],
    }),
  ],
  module: {
    rules: [
      {
        test: /.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  mode: 'development',
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.WatchIgnorePlugin([/\.js$/, /\.d\.ts$/]),
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'server.js',
  },
};

ドキュメントとの設定に new webpack.WatchIgnorePlugin([/\.js$/, /\.d\.ts$/]) を追加しました。

.js と型定義ファイルの.d.ts は変更を検知する必要がないので外すようにしました。

変更検知の間隔を遅くしたい際は、webpack/hot/poll?100 を100から300に変えるといいと思います。

HMRの有効化

NestJSの開発サーバー側でHMRを有効化する必要があります。

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}
bootstrap();

ドキュメントではwebpackの module 変数の型エラーを回避するため、

declare const module: any;

の記述をしていましたが、 @types/webpack-env をインストールしているためここでは必要ありません。

webpackサーバー起動設定

最後にファイルの変更検知をし、HMRを実行するためのwebpackサーバーを起動をnpmコマンドで実行するための設定を行います。

"scripts" オプションにある"start" を更新、 "webpack" コマンドを追加します。

// package.json
{
  "scripts": {
    ...
    "start": "node dist/server",
    "webpack": "webpack --config webpack.config.js",
    ...
}

これでHMR環境が整いました。

webpackサーバー起動

webpackサーバーの起動コマンドを実行して、ファイルの変更検知 + HMRを行います。

$ npm run webpack

その後、ターミナルを別タブなどで開いて、NestJSの開発サーバーを起動します。

$ npm run start

この状態でどれかファイルを変更すると、コンパイルが走りアプリケーションが挙動も変更されていると思います。

npm run start:dev で開発する時よりも明らかにコンパイルが速いはずです。

TypeORMでデータベースに接続できない問題

TypeORMを導入しているプロジェクトでHMR環境で開発している場合、以下のようなデータベースに接続できないエラーが発生します。

EPERM: operation not permitted, scandir ...

これは以下のように、データベースの設定を書いていた場合 __dirname がHMRの時にうまく機能していないのが原因です。

// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'MY_HOST',
      port: 3306,
      username: 'MY_USERNMAE',
      password: 'MY_PASSWORD',
      database: 'MY_DATABASE',
      entities: [join(__dirname, '**/**.entity{.ts,.js}')],
      synchronize: true,
    })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

この解決方法として以下のようなワークアラウンドができます。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { getMetadataArgsStorage } from 'typeorm'; // <-変更

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'MY_HOST',
      port: 3306,
      username: 'MY_USERNAME',
      password: 'MY_PASSWORD',
      database: 'MY_DATABASE',
      entities: getMetadataArgsStorage().tables.map(tbl => tbl.target), // <- 変更
      synchronize: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

ファイルからではなくTypeORMの getMetadataArgsStorage() メソッドからテーブルを取得、そこから紐づいているクラスを呼び出して解決しています。

参考: https://github.com/nestjs/nest/issues/755#issuecomment-496793495

最後に

フロントエンドをTypeScriptでの開発を行っていくことで、より堅牢でリッチなアプリケーションを作ることができます。

同様に、バックエンド側も合わせてTypeScriptにすることは処理やデータ構造の共通化をするという意味でもかなり魅力的なことである思っています。

その中でもNestJSはAngularに影響を受けているためか、きっちりした設計でバックエンドアプリの開発を行えるので注目してるのでこれからもNestJSについては書いていこうと思います。

次回は書くと言って書いていなかったTypeORMについての記事を書きたいと思います。

Daiki Urata

Daiki Urata

フロントエンド/モバイルアプリなどを主に開発しています。