DockerでTypeScript×Node.js×Expressの環境構築

# やること
Node.jsとExpressの環境構築をDocker上で行います。
また、TypeScriptでの開発を行うためにTypeScript環境のインストールも行います。

まずNode.js×Expressの環境構築後、TypeScriptをインストール、設定します。

# 準備
### Docker Desktopのインストール
[こちら](https://www.docker.com/products/docker-desktop)からDocker Desktopをインストールします。
Mac/Windows/Linuxに対応しています。

### VSCodeのインストール
[こちら](https://code.visualstudio.com/download)からVSCodeをインストールします。
他のエディタでも問題ないですが、諸々使いやすいのでおススメします。
MacやLinuxをお使いの方であればvimでも問題ありません。

# Node.js×Expressの環境構築
以下、ホスト上の作業は :file_folder: 、Dockerコンテナ上の作業は :whale2: で表します。

### :file_folder: ディレクトリ作成
ルートディレクトリ及びソース格納用のsrcディレクトリを作成します。
もちろん、エクスプローラやFinderで直接作成しても問題ありません。

```shell
# ルートディレクトリ作成
$ mkdir docker-app

# フォルダ移動
$ cd docker-app

# ソース格納用ディレクトリ作成
$ mkdir src
```

### :file_folder: docker-compose.ymlの作成
Dockerでの環境構築には基本的に`Dockerfile`と`docker-compose.yml`の2つが必要です。
それぞれの主な役割は以下の通りです。

- `Dockerfile`
　用いるDockerイメージ（例えばNode.jsやApache、Java等動作に必要なパッケージ等）について記述するファイル。
　公開されているDockerイメージをそのまま使う場合は必要なく、カスタマイズしたい場合に作成する。

- `docker-compose.yml`
　コンテナの定義やマッピングするポート等コンテナに関する設定を記述するファイル。
　必ず記述する。

今回、Node.jsは公開されているイメージを使用するため、`Dockerfile`は作成しません。
まず`docker-compose.yml`を作成します。
※下記のコマンドはWindowsでは使用できないため、エクスプローラやVSCode上で作成してください。

```shell
# ファイル作成
$ touch docker-compose.yml

# ファイルを開く
$ vim docker-compose.yml
```

```yml:docker-compose.yml
version: '3'
services:
    app:
        # 起動イメージ
        image: node:16
        # 環境変数
        environment:
            - DEBUG=app:*
        tty: true
        # ホスト側のポート：コンテナのポート
        ports:
            - '3000:3000'
        # ホスト側のsrcをコンテナのappにマウント
        volumes:
            - ./src:/app
        # 起動時のカレントフォルダ
        working_dir: /app
        # 起動後に実行するコマンド
        command: npm start
```

重要な箇所は `# ホスト側のsrcをコンテナのappにマウント`の部分で、通常Dockerコンテナを停止するとコンテナ上で作成した各種ファイルは削除されますが、上記の記述を行うことでコンテナ起動時に再度ファイルがマウント（反映）されます。

また、Dockerコンテナ上で作成・変更したファイルもこちらに記述した場所に反映されます。

基本、ホスト上とコンテナ上でファイルの同期をとるための記述と考えればOKです。

### :file_folder: Dockerコンテナ起動
`docker-compose.yml`を作成できたので、早速Dockerコンテナを起動します。

```shell
# Node.jsコンテナを起動
$ docker-compose run --rm app /bin/bash
```

docker-composeコマンドにはrun、up、build等があります（違いは[こちら](https://qiita.com/yuta-38/items/29facbe3f236bf250651)等を参照）。
runの場合は単一コンテナのサービスに対して１回コマンドを実行します。

上記ではappコンテナ（Node.jsコンテナ）でbashを起動しています。

rmオプションはDockerコンテナ停止時にコンテナを削除する機能で、停止したコンテナが残り続ける問題を解決するためのオプションです。

### :whale2: Expressアプリケーション作成
上記起動がうまくいけば、Dockerコンテナ上でbashが起動した状態になります。
Node.jsコンテナが起動しているので、早速Expressをインストールしていきます。

```shell
# express-generatorでひな形を生成
root@2e490c20618c:/app# npx express-generator
```

※npxコマンドは`package.json`に定義されていないものでも実行可能なコマンド。
上記コマンドを実行すると、routeやviewフォルダ、`package.json`や`app.js`等が作成されます。
コンテナ上で作成されると同時に、ホスト上のsrc配下にも同じフォルダやファイルがこの時点で作成されます。

次に、`package.json`に記述されている依存パッケージをインストールします。
さらにこのままだと、ホスト上で変更したソースコードを反映させるために毎回コンテナの再起動が必要になるため、ソースコードの変更を検知して必要な時にアプリケーションを再起動する`nodemon`をインストールする。

```shell
# 依存パッケージをインストール
root@2e490c20618c:/app# npm install
# nodemonをインストール
root@2e490c20618c:/app# npm install -D nodemon
```

インストールが完了したらCtrl+Dキーを押下してコンテナを停止します。

### :file_folder: nodemonに関する設定変更
nodemonをコンテナ起動時のデフォルトコマンドに設定するため、`package.json`のscriptsと`docker-compose.yml`のcommandを以下のように変更します。

```json:package.json
"scripts": {
  "dev": "nodemon ./bin/www",
  "start": "node ./bin/www"
},
```

```yml:docker-compose.yml
# 起動後に実行するコマンド
command: npm run dev
```

### :file_folder: Expressアプリケーション起動
ここまでできれば、この時点で一旦jsでアプリケーション起動が可能なので、コンテナを起動します。
upコマンドで複数コンテナを一度に起動できるため、基本的にはこのコマンドを使います。

```shell
# Expressアプリケーションを起動
$ docker-compose up
```

ブラウザから[http://localhost:3000](http://localhost:3000)にアクセスします。
下記のような初期画面が表示されていれば成功です。

![express2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635329/2f8d1efa-6ac5-ae0f-7c03-401f68315019.png)


# TypeScriptの環境構築

### :file_folder: Dockerコンテナ起動
Dockerコンテナを起動して、TypeScriptをインストールします。

```shell
# Node.jsコンテナを起動
$ docker-compose run --rm app /bin/bash
```

### :whale2: TypeScriptインストール
次に、TypeScriptやExpress、インポートしてるパッケージの型定義をインストールしてtsconfigを作成します。

```shell
# TypeScriptと型定義をインストール
root@2e490c20618c:/app# npm install -D typescript @types/express @types/cookie-parser @types/morgan @types/http-errors
# tsconfig作成
root@2e490c20618c:/app# npx tsc --init
```

この段階で、ホスト側にも`tsconfig.json`が作成されますので、ここからホスト側でTypeScript用の設定をします。
Ctrl+DでDockerコンテナを停止します。

### :file_folder: 出力先フォルダ設定
tscコマンドでTypeScriptからJavaScriptにコンパイルされますが、デフォルトではtsファイルと同じ階層にjsが作成されます。
tsファイルとjsファイルが同階層に作成されると、管理が煩雑になるためdistフォルダを作成し、そこに出力されるようにします。

```shell
# 出力先フォルダ作成
$ mkdir dist
```

distフォルダに出力されるようにするため、`tsconfig.json`のoutDirを以下のように書き換えます。
また、今回jsファイルと共存させるため、allowJsも有効にします。

```json:tsconfig.json
"allowJs": true,       /* Allow javascript files to be compiled. */
"outDir": "./dist",    /* Redirect output structure to the directory. */
```

次に、Expressが起動した際に参照する先をdistフォルダに変更するため、binフォルダのwwwの該当部分を以下のように書き換えます。

```javascript:www
/**
 * Module dependencies.
 */

var app = require('../dist/app');
var debug = require('debug')('app:server');
var http = require('http');
```

### :file_folder: TypeScriptソースコード作成
まず、`app.js`及びroutesフォルダ配下の`index.js`の拡張子をtsに書き換えます。
さらにこのままでは一部のソースがTypeScriptコンパイルに失敗するので、`app.ts`及び`index.ts`の該当の部分を型定義を追加するために下記のように書き換えます。
`index.ts`はパスの指定をdist配下からではなく、プロジェクトルートからの相対パスにしたいため、`__dirname`を削除します（コンパイルされるのはtsファイルのみのため、publicフォルダやviewフォルダに格納される静的ファイルはdist配下に格納されない）。

```typescript:app.ts
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index'); // 変更箇所
var usersRouter = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join('views'));  // 変更箇所
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join('public')));  // 変更箇所

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req: any, res: any, next: any) {  // 変更箇所
  next(createError(404));
});

// error handler
app.use(function(err: any, req: any, res: any, next: any) {  // 変更箇所
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;
```

```typescript:index.ts
var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req: any, res: any, next: any) {  // 変更箇所
  res.render('index', { title: 'Express' });
});

module.exports = router;
```

### :file_folder: TypeScriptコンパイル自動化設定
現段階では、ホスト側でtsファイルを書き換えた際、Dockerコンテナ起動時に手動でコンパイルを行う必要があるため、自動化します。
そのために、コンテナ起動時のコマンドを指定する`docker-compose.yml`のcommandを書き換えます。
コンパイルしてからnodemonを起動するようにします。

```yml:docker-compose.yml
# 起動後に実行するコマンド
command: sh -c 'npx tsc; npm run dev'
```

### :file_folder: Expressアプリケーション起動
ここまでで、一通りの環境構築完了です。
コンテナを起動しましょう。

```shell
# Expressアプリケーションを起動
$ docker-compose up
```

ブラウザから[http://localhost:3000](http://localhost:3000)にアクセスします。
下記のような初期画面が表示されていれば成功です。

![express2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635329/2f8d1efa-6ac5-ae0f-7c03-401f68315019.png)

この後一度Ctrl+Dでコンテナを停止し、`index.ts`のtitle等を書き換え、再度コンテナを立ち上げた際に変更が反映できていれば成功です！

# 参考
[DockerでNode.jsアプリケーションを開発する (1) Express.jsをコンテナ内で動かす](https://ishida-it.com/blog/post/2019-11-21-docker-nodejs/)
[TypeScript+Expressの快適な開発環境を作ってみた](https://blog.mamansoft.net/2019/08/12/develop-express-with-typescript-cool-environment/)