TypeScriptでExpress.js開発するときにやることまとめ (docker/lint/format/tsのまま実行/autoreload)


# やること

ステップごとに分けてあるので、導入したい箇所だけつまみ食いすることも可能です。
気力が残っていれば、追記で本番環境の運用についても書きたいです…

 1. express.jsをDockerで起動
 2. TypeScript導入
 3. TSLint + Prettier導入
 4. node-ts導入: dev環境はトランスパイルなしでtsファイルのまま開発
 5. nodemon導入: dev環境はファイル変更を検知して自動リロード


**記事ではnode.js 8.12を使用しています。**
8系から、npm install時に自動的に`--save`オプションが付くようになっていますので、
記事内で「あれ？--saveしてなくない？」とならないように一言補足。


# まずは最小構成でexpress.js起動

### 準備

(追記)
エディタ補完を効かせるには結局MacにNode.js入れないといけなかったりしましたが、
VSCodeがDockerコンテナのリモート開発に対応したりして潮目も変わってきたので
過激派用の設定も追記しました。
[Dockerで立ち上げた開発環境をVS Codeで開く!](https://qiita.com/yoskeoka/items/01c52c069123e0298660)


## A. MacにNode.js入れている場合

PJフォルダ(仮にts-express)を作成し、とりあえずMac環境でinit

```bash
npm init -y
```

## B. MacにNode.js入れない硬派な場合


PJフォルダ(仮にts-express)を作成し、空のjsonを作成
(何はともあれpackage.jsonがないとDockerイメージのビルドに失敗するので)

```bash
echo "{}" >> package.json
```

### Docker開発環境整備

`Dockerfile` 作成

dockerのなんやかんやは素晴らしい記事がたくさんあるので、簡潔な説明に留めます…

```dockerfile:Dockerfile
# ベースイメージはお好みで普通のでもalpineでも。
FROM node:8.12.0-slim
ENV APP_ROOT /app/

WORKDIR $APP_ROOT

# package.jsonとpackage-lock.jsonを先にコピー。
# package*.jsonだけを先に個別コピーすることで、パッケージ変更時は`RUN npm install`が走るが
# それ以外のファイル変更時は同コマンドにはキャッシュ利用で飛ばされるため、ビルド時間を短縮できる。
COPY package*.json $APP_ROOT
RUN npm install

# その他ファイルをコピー。
COPY . $APP_ROOT
```

`docker-compose.yml` 作成
dockerコマンド叩くときに毎回引数つけるのが面倒なので、docker-compose作ります。


```yml:docker-compose.yml
version: '2.1'
services:
  web:
    build:
      context: .
      dockerfile: ./Dockerfile
    command: npm run dev
    environment:
      NODE_ENV: development
    ports:
      - '3000:3000'
    volumes:
      - .:/app
```

入る

```bash
docker-compose run --rm web bash
```

**以降、別段コメントがなければbashコマンドはdocker内で実行します。**

Mac側でnpm initしていない場合はここでやる

```bash
npm init
```

expressインストール

```bash
npm i express
```


テスト用`src/index.js`作成

```js:src/index.js
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));
```

package.jsonにdev実行コマンドを追加

```js:package.json
"scripts" {
 "dev": "node src/index.js"
}
```

起動テスト

```bash
# Macの別窓terminalで実行
docker-compose up --build web
# バックグラウンド実行したいときは-dオプション
# docker-compose up -d --build web
```

ブラウザで`http://localhost:3000/`を開くと Hello World! が出ます :tada: 
出たら一度 `docker-compose down web` で止めましょう。
-dオプション無しで起動してる場合はCtrl+cでも止められます。


(追記)
※`docker-compose up`は、2回目以降は実は毎回ビルドしなくても良かったりします。

docker-compose.ymlファイルのvolumesに書いてある通りファイルの変更は
dockerの中でいじろうがホスト側でいじろうが常に双方向に更新されます。
なので、キャッシュが効いて高速にビルドされるとはいえ、数秒の待ちが煩わしい人は
普段は--buildなし、何か挙動がおかしくなったら--build付けて再ビルドが
お手軽かなと思います。堅実に生きたい人は常に--buildで。


# TypeScript導入

TypeScriptと型情報を追加

```bash
npm i --save-dev typescript @types/express
```

`src/index.js`を`src/index.ts`に名前変更
とりあえずトランスパイル出来ることだけを確認したいので、中身は変更しない。

`/dist`ディレクトリを作成

```
mkdir dist
```

`tsconfig.json` を作成

requireの記法を使うなら "module": "commonjs"
importの記法を使うなら "module": "es6" (TSなら普通こっちですかね。)


```json:tsconfig.json
{
    "compilerOptions": {
        "outDir": "./dist",
        "sourceMap": true,
        "module": "es6",
        "target": "es5",
        "moduleResolution": "node",
        "removeComments": true
    },
    "include": [
        "./src/**/*"
    ]
}
```

モジュールのインポートがcommonjsの場合はsrc/index.tsの1行目を書き換え

```js
// const express = require('express'); // これを
import * as express from "express";    // こう
```

`package.json`のscriptsを編集

```js:package.json
"scripts": {
  // distから走るように修正。
  "dev": "node dist/index.js",
  // 追加。TypeScriptをグローバルではなくローカルインストールしているため、npm run tscでコンパイルがちゃんと走るようにする
  "tsc": "tsc"
}
```

トランスパイルする

```bash
npm run tsc
```

これでdistにトランスパイル後のファイルが出来上がるので、改めて走らせてみる

```bash
# Macの別窓terminalで実行。すでに走っている場合はCtrl+Cで一度止める。
docker-compose up --build web
```

# TSLint + Prettier導入(入れたい方のみ)

(2019/11/11追記) 

[こちら](https://qiita.com/suzuki_sh/items/fe9b60c4f9e1dbc5d903) の記事にあるように、現在はTSLintからESLint TypeScript Pluginへの移行が推奨されています。
> TSLint 開発チームは ESLint のプラグインとして TSLint の機能を統合していく typescript-eslint プロジェクトを開始し、TSLint は 2019年中に非推奨となる予定である ことが発表されました。

よって、長期的にメンテナンスが見込まれる開発の場合はLinter/Formatter導入について [VSCodeでESLint+@typescript-eslint+Prettierを導入する（v2.0.0修正版）](https://qiita.com/madono/items/a134e904e891c5cb1d20) 等を参考にして下さい :bow: 


---------- 以下、既存のTSLint + Prettier導入記事 ----------

(ESLintの場合はこのブログ記事が参考になります。 [ESLint(あるいはTSLint)とPrettierを併用する](https://tech-1natsu.hatenablog.com/entry/2018/01/07/154941)）

リンターとフォーマッターの導入です。
両方入れる方法はいくつかあるのですが、ここではTSLintにPrettierの処理もまかせる方式で行きます。
この方式だと、設定ファイルが`tslint.json`一箇所にまとまって見通しが良くなります。

```bash
npm i -D tslint prettier tslint-config-prettier tslint-plugin-prettier
```

〜簡潔に説明〜

 - `tslint` リンター本体
 - `prettier` フォーマッター本体
 - `tslint-config-prettier` TSLintとPrettierの重複処理を無効化
 - `tslint-plugin-prettier` ESLintのルールとしてPrettierを読み込む


（optional）ベースルールを使用したい人は導入します。僕の場合はeslintのstandardが好きなので、
そのルールをTSに合うようにカスタマイズされた`tslint-config-standard`を使用します。

```bash
npm i -D tslint-config-standard
```

`tslint.json`ファイルを作成し、設定を書きます。
extendsの`tslint-config-standard`は上記で導入したルールに読み替えてください。
入れなかった場合は`tslint-config-prettier`だけ記述すればOKです。

```js:tslint.json
{
  "rulesDirectory": ["tslint-plugin-prettier"],
  "extends": ["tslint-config-standard", "tslint-config-prettier"],
  "rules": {
    // TSLintに任せる方法の場合、prettier設定もここに書くことになります。
    // 設定内容はstandardだとsingleQuoteとsemiだけ気をつければ多分大丈夫です。
    // また、完全に僕の好みですが行あたり80文字は短すぎるので長めにしてあります。
    "prettier": [
      true, {
        "singleQuote": true,
        "semi": false,
        "printWidth": 120 // defaultの80って短くないですか？
      }
    ]
  }
}
```

上記を動かせるように、lintコマンドを追加します。

```js:package.json
"script" {
  "lint": "tslint --exclude **/*.d.ts --project . --fix 'src/**/*.ts' 'test/**/*.ts'"
}
```

`--project .`は付けないと、`rule requires type information`の警告が大量に出るため付けています。

テストディレクトリを含めるかどうかはお好みで。
またlintコマンドのオプションは色々な指定方法があるので、PJに合わせて適宜変更してください。

では、作成したlintコマンドテストをテスト。

```bash
npm run lint
```

自動修正できないものは無理ですが、修正できるものはこれで一発修正されます。
例えば、standardルールなら、最初に作ったsrc/index.tsの;が全部吹っ飛んだはずです。

(optional)VSCodeの人
[TSLint](https://marketplace.visualstudio.com/items?itemName=eg2.tslint)プラグインを導入し、設定で `"tslint.autoFixOnSave": true` にすれば
保存のたびにLint+Prettierしてくれます。

# ts-nodeを使ってトランスパイルなしで開発

ここまでくればTSでコードを書いていけるようになりましたが、コードを変更するたびに
`npm run tsc`して`docker-compose up web`し直さなければなりません。それは流石にキチィ。

というわけで、ts-nodeを入れます。こいつを入れることで、nodeコマンドでjsファイルを
実行するのと同じようにts-nodeでtsファイルを実行できるようになります。

インストール

```bash
npm i -D ts-node
```

設定

`npm run dev`コマンドでnodeではなくts-nodeを使用し、srcのtsファイルを直接読むようにします。
また、本番環境ではトランスパイル後の生jsを走らせると思うのでdistからの実行コマンドを別で用意します。
ts-nodeは例によってローカルインストールのため、パスが通るように手動でscriptに追加します。

**追記:** ts-nodeの本番環境運用も試しており、結論から言うと起動時間が若干長いという以外の
オーバーヘッドは殆ど無いので、本番実行も `ts-node ./src/index.ts` でも良いと思います。

```js:package.json
"scripts": {
  "start": "node dist/index.js",    // 本番実行用のコマンド。これの前にtscも走らせること。
  "dev": "ts-node ./src/index.ts",  // 開発環境用のコマンド。
  "ts-node": "ts-node"
}
```

テスト

```bash
# Macの別窓terminalで実行。すでに走っている場合はCtrl+Cで一度止める。
docker-compose up --build web
```

トランスパイルしなくても、直接tsファイルを読み出すようになりました。


# 変更検知して自動リロード

tsファイルを直読めるようになったとはいえ、まだこのままではファイルに変更が加わるたびにサーバーを止めて
'npm start'し直さなければなりません。それは流石にキチィ。

というわけで、nodemonを入れます。こいつを入れることでコードの変更を検知して
サーバーをオートリロード出来るようになります。

インストール

```bash
npm i -D nodemon
```
設定ファイルnodemon.jsonを新規作成

```js:nodemon.json
{
  "watch": ["src"],
  "ext": "ts",
  // テストファイル等、リロード対象にしたくない物があればここに追加
  "ignore": ["tests/**/*.ts"],
  // 実際に実行するコマンド。先程のts-nodeのコマンドです。
  "exec": "npm run ts-node ./src/index.ts"
}
```

devコマンドでnodemonが起動するように修正

```js:package.json
"scripts": {
  "dev": "nodemon -L"
}
```


テスト

```bash
# Macの別窓terminalで実行。すでに走っている場合はCtrl+Cで一度止める。
docker-compose up --build web
```

試しに`src/index.js`のconsole.logのコメントをちょっといじって保存してみて下さい。
docker-composeの実行画面にリロード状況が出て、リロード後ブラウザでlocalhost:3000を
開き直すと変更が適用されているのが分かると思います。

# おわり

これで、快適なTypeScript + Express.js開発環境を手に入れることができました。

Dockerfile, docker-compose.yml, その他lint, prettier等の各種設定は
プロジェクトにより色々変わってくると思うので、この記事はあくまでベースとして使用して
修正加えてください。

#### 以下もお供にどうぞ

コントローラー
 - [TypeScriptでExpress.jsのコントローラー部分をクラスベースで書く](https://qiita.com/yuukive/items/331aa21654bb8b20a34d)

モデル
 - [TypeScriptのORマッパーならTypeORM](https://qiita.com/yuukive/items/cf1dd715b5bf5525f8df)
 - [TypeORMのテストで楽をする](https://qiita.com/yuukive/items/b970494fd0f8d2df3784)