TypeScript で Express サーバを実装するためのボイラープレートを作った

サーバサイド JS であれば、個人的には TypeScript がなくても書けるのだが、複数人で開発するとなると、型チェックを導入できた方が安全な実装になる。

ということで今回は、Express サーバを TypeScript で実装するためのボイラープレートプロジェクトを作ってみた。

目次

コード全量

いきなりだがコード全量は以下に置いた。

ご自由にご利用ください。

プロジェクト作成の流れ

だいたい次のような流れでプロジェクトを作っていった。

package.json を生成する

$ npm init -y

TypeScript 環境を作る

$ npm i -D typescript @types/node

# 以下で tsconfig.json を生成する。あとで内容を編集する
$ npm run tsc -- --init

Express を入れる

$ npm i -S express
$ npm i -D @types/express

# コードを書いていく
$ mkdir src
$ touch src/index.ts

# コードをビルドする時はこんな感じ (後で npm run build にする)
$ npm run tsc

# ビルドしたコードを実行する時はこんな感じ (後で npm start にする)
$ node dist/index.js

ts-node : トランスパイルなしで動作させるためのツール

$ npm i -D ts-node

# 使う時はこんな感じ (後で npm run start-ts にする)
$ npm run ts-node src/index.ts

ts-node-dev : ts-node のライブリロード開発対応版

$ npm i -D ts-node-dev

# 使う時はこんな感じ・ファイルの変更監視をしてくれる (後で npm run dev にする)
$ npm run ts-node-dev src/index.ts

ESLint : Linter

$ npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser

# .eslintrc.js を生成する。あとで内容を編集する
$ npm run eslint -- --init

# 使う時はこんな感じ (後で npm run lint にする)
$ npm run eslint -- --ext '.js,.ts' ./src/**

# Lint + 自動修正を行う場合はこんな感じ (後で npm run lint-fix にする)
$ npm run eslint -- --fix --ext '.js,.ts' ./src/**"

Jest : ユニットテスト

$ npm i -D jest ts-jest @types/jest

# Express API をモック化するために入れておく
$ npm i -D supertest @types/supertest

# 設定ファイルを作成する・内容は後述する
$ touch jest.config.js

# テストファイルを書く
$ mkdir test
$ touch test/index.spec.ts

# 使う時はこんな感じ (後で npm test にする)
$ npm run jest -- --coverage
# coverage/ ディレクトリにカバレッジレポートが出力される

# ライブリロードでテストを行う場合はこんな感じ (後で npm run test-watch にする)
$ npm run jest -- --coverage --watch

supertest を使う時は、ソースコードとテストコードをそれぞれ次のように記述すること。

import * as express from 'express';

const app = express();
app.get('/', (req, res) => { res.send('Hello World'); });

// ユニットテスト時の呼び出しでなければサーバを起動する
if(!module.parent) {
  app.listen(process.env.PORT || 8080, () => {
    console.log('Listening');
  });
}

// ユニットテストのために app をエクスポートする
export default app;
import * as supertest from 'supertest';

import app from '../src/index';

describe('サーバのテスト', () => {
  it('Path [/] のテスト', async () => {
    const res = await supertest(app).get('/');
    expect(res.text).toBe('Hello World');
  });
});

こんな感じ。app.listen() をユニットテスト時に実行しないようにしておく。以前も Jasmine を使う際の解説に書いたが、Jest でも同様だった。

各種設定ファイル

本プロジェクトで必要になる設定ファイルは以下のとおり。

それぞれの内容を記載していく。

package.json

前述のコマンドを叩いていったあと、scripts セクションを書いていく。

{
  "name": "typescript-express-boilerplate",
  "private": true,
  "scripts": {
    "eslint": "eslint",
    "jest": "jest",
    "ts-node": "ts-node",
    "tsc": "tsc",
    "?start": "echo 'トランスパイル済の Express サーバを起動する・事前に npm run build を実行し ./dist/ ディレクトリにコードを生成しておくこと'",
    "start": "node ./dist/index.js",
    "?start-ts": "echo 'ts-node で ./src/ 配下のコードを直接実行する'",
    "start-ts": "ts-node ./src/index.ts",
    "?dev": "echo 'ライブリロード開発を開始する・ts-node-dev で ./src/ 配下のコードを直接実行する'",
    "dev": "ts-node-dev ./src/index.ts",
    "?lint": "echo 'ESLint を実行する (型チェックはできないので npm run build を利用する)'",
    "lint": "eslint --ext '.js,.ts' ./src/**",
    "?lint-fix": "echo 'ESLint を実行し、自動修正できる箇所は修正する'",
    "lint-fix": "eslint --fix --ext '.js,.ts' ./src/**",
    "?test": "echo 'ユニットテストを実行する・結果は ./coverage/ ディレクトリに出力される'",
    "test": "jest --coverage",
    "?test-watch": "echo 'ユニットテストを監視実行する'",
    "test-watch": "jest --coverage --watch",
    "?build": "echo 'TypeScript をトランスパイルする・設定は ./tsconfig.json を参照'",
    "build": "tsc"
  },
  "dependencies": {
    "body-parser": "1.19.0",
    "express": "4.17.1"
  },
  "devDependencies": {
    "@types/express": "4.17.6",
    "@types/jest": "25.2.3",
    "@types/node": "14.0.5",
    "@types/supertest": "2.0.9",
    "@typescript-eslint/eslint-plugin": "3.0.0",
    "@typescript-eslint/parser": "3.0.0",
    "eslint": "7.1.0",
    "jest": "26.0.1",
    "supertest": "4.0.2",
    "ts-jest": "26.0.0",
    "ts-node": "8.10.1",
    "ts-node-dev": "1.0.0-pre.44",
    "typescript": "3.9.3"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    
    // 以下、デフォルトから設定を変更したところ
    
    // トランスパイル後のファイルを出力するディレクトリを指定する
    "outDir": "./dist",
    // モジュール解決の方法を指定する
    "moduleResolution": "node",
    // import * as を認識させるため false にする
    "esModuleInterop": false
  },
  // トランスパイル対象ディレクトリを指定する
  "include": [
    "./src/**/*"
  ]
}

./dist/ にビルド後のファイルを出力するので、.gitignoredist/ を無視しておこう。

.eslintrc.js

TSLint は廃止予定なので、ESLint を使用するよ。

module.exports = {
  env: {
    es6: true,
    node: true
  },
  extends: [
    'plugin:@typescript-eslint/recommended'
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: 'module'
  },
  plugins: [
    '@typescript-eslint'
  ]
};

独自にルール設定したい場合は rules: [] を追加する。何のルールも設定しない空配列だとエラーになるので、その場合は記述しないでおく。

jest.config.js

module.exports = {
  transform: {
    '^.+\\.ts$': 'ts-jest'
  }
};

Jest は --coverage オプションを付けると ./coverage/ ディレクトリにカバレッジレポートを出力するので、.gitignorecoverage/ を無視しておこう。

ひとまず OK

実装はこんな感じ。

あとは src/ 配下にお好みで TypeScript を書いていくだけ。

Jest はデフォルトで src/ ディレクトリ内の .ts ファイルに対応する .spec.ts ファイルを探すので、テストファイルが1対1で揃っていないとワーニングが出る点に注意。

その他のオプション

ファイルの変更監視に ts-node-dev ではなく nodemon を使う

ファイルの変更監視、ライブリロード開発のために ts-node-dev パッケージを使ったが、世間的には nodemon の方が知られているかも。ただし、動作は ts-node-dev の方が格段に速い。

一応 nodemon を使う方法も紹介。

$ npm i -D nodemon
$ touch nodemon.json
{
  "watch": [
    "./src/"
  ],
  "ext": "ts",
  "exec": "npm run ts-node ./src/index.ts"
}

ESLint に Prettier を追加する

Prettier は、Linter というよりはコード・フォーマッタといえるツール。ESLint 越しに呼び出せるので連携する人も多いだろう。

強制力が強すぎるのが嫌で自分は入れなかったが、導入する際は前述の手順に加えて以下を行う。

$ npm i -D prettier eslint-config-prettier eslint-plugin-prettier
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/eslint-recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
    'prettier/@typescript-eslint'
  ],
  plugins: ['@typescript-eslint'],
  env: {
    node: true,
    es6: true
  },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: 'module'
  },
  rules: {
    // Prettier のルールを上書きする場合は次のように書く
    'prettier/prettier': [
      'error',
      {
        // シングルクォートを使う
        singleQuote: true,
        // 配列の最後の要素の後ろにカンマを置かなくて良い
        trailingComma: 'none'
      }
    ]
  }
};

以上

ざっとコードで説明した。何のパッケージが必要なのか整理していかないと分かりにくいので、もうこうやってボイラープレートにまとめておくのが良さげ。

参考文献