express+TypeScriptでAPIを構築する際にやったこと

##はじめに

今まではPHPでAPIを構築することが多かったが、メンバーのスキルセットがjs寄りだったため、
express(node.js)でAPIを構築することになったので、その際のメモ。

##環境
* Windows10
* Docker for Windows：3.0.0
* node.js：v14.15.1
* npm：6.14.8
* TypeScript：4.1.3
* express：4.16.1
* MySQL：8.0.21
* eslint：7.15.0
* prettier：2.2.1

##Dockerで動作環境を構築
docker-compose.ymlはこんな感じ

```docker-compose.yml
version: "3"
services:
  mysql:
    image: mysql:8
    command: --default-authentication-plugin=mysql_native_password
    env_file: ./mysql/.env
    ports:
      - 3306:3306
    volumes:
      - ./mysql/data:/var/lib/mysql
      - ./mysql/conf/my.cnf:/etc/my.cnf
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    env_file: ./phpmyadmin/.env
    ports:
      - 8080:80
  app:
    image: node:12
    env_file: ../api-dir/.env # Docker用のディレクトリと同列にAPIのディレクトリを配置
    tty: true
    ports:
      - 3000:3000
    volumes:
      - ../api-dir:/app
    working_dir: /app
    command: npm start
volumes:
  mysqldata:
```

## expressの用意
まずDocker用のディレクトリと同列にAPIのディレクトリを作成する

```
$ cd ../
$ mkdir api-dir
$ cd api-dir
```

必要なモジュールをnpmでインストール（主なもの）

* express：node.jsのフレームワーク
* express-validator：バリデーション
* knex：クエリビルダー、マイグレーション、シーディング等
* winston：ログ
* jest：UnitTest
* supertest：jestでのAPIリクエストテスト用
* json2csv：csv変換
* nodemon：コード変換時の自動再起動
* ts-node：TypeScriptのトランスパイル
* eslint：静的解析
* prettier：コード整形

node.jsのクエリビルダーで良いものがないか探し回った末、knexにたどり着いた。
Laravelに慣れていたが、違和感なく使えた。
[公式ドキュメント](http://knexjs.org/)も充実しているのでオススメ。

## 各種設定ファイルの作成

```package.json（一部抜粋）
"scripts": {
    "start": "NODE_ENV=development npx nodemon --exects-node", // 起動コマンドを変更
    "lint:fix": "eslint */**/*.ts *.ts *.json --fix",
    "db:down": "knex --knexfile knexfile.ts migrate:rollback --all --env test",
    "db:migrate": "knex --knexfile knexfile.ts migrate:latest --env test",
    "db:seed": "knex --knexfile knexfile.ts seed:run --env test",
    "db": "npm run db:down && npm run db:migrate && npm run db:seed",
    "test": "jest --config jestconfig.json --coverage --verbose --runInBand --detectOpenHandles",
}
```

```nodemon.json
{
  "watch": ["src", "seeds", "migrations", "tests"],
  "ext": "ts",
  "exec": "ts-node ./bin/www"
}

```

```jestconfig.json
{
  "testMatch": ["**/?(*.)+(spec|test).+(ts|tsx|js)"],
  "transform": {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
  "setupFilesAfterEnv": ["./jest.setup.js"] // タイムアウトの秒数を調整
}
```

```jest.setup.js
jest.setTimeout(5000)
```

```knexfile.ts
if (!require.cache['dotenv']) {
  require('dotenv').config()
}

module.exports = {
  development: { // 開発用
    client: 'mysql',
    connection: {
      host: process.env.MYSQL_SERVER,
      user: process.env.MYSQL_USER,
      password: process.env.MYSQL_PASSWORD,
      database: process.env.MYSQL_DATABASE,
      port: process.env.MYSQL_PORT,
    },
    migrations: {
      extension: 'ts',
      tableName: 'knex_migrations',
    },
  },

  test: { // Jest用
    client: 'mysql',
    connection: {
      host: '127.0.0.1',
      user: process.env.MYSQL_USER,
      password: process.env.MYSQL_PASSWORD,
      database: process.env.MYSQL_DATABASE,
      port: process.env.MYSQL_PORT,
    },
    migrations: {
      extension: 'ts',
      tableName: 'knex_migrations',
    },
  },
}
```

```winston.ts（おまけ）
import fs from 'fs'
import path from 'path'
import moment from 'moment'
const winston = require('winston')
require('winston-daily-rotate-file')

const logDirectory = path.join(__dirname, './logs')
//指定したディレクトリが存在するか？
fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory)

const transport = new winston.transports.DailyRotateFile({
  filename: path.join(logDirectory, './error-%DATE%.log'),
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,
})

transport.on('rotate', function (oldFilename, newFilename) {
  // do something fun
})

// creates a new Winston Logger
const logger = new winston.createLogger({
  level: 'info',
  transports: [transport],
  exitOnError: false,
})

logger.errorLog = (req, res, err) => {
  logger.error(
    `[${moment().format('YYYY-MM-DD HH:mm:ss')}] ${req.method} - ${
      res.statusCode
    } - ${req.originalUrl} - ${req.ip} - ${err.message}`,
  )
}
module.exports = logger
```

```.eslintrc.json
{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "prettier/@typescript-eslint",
    "plugin:prettier/recommended"
  ],
  "parserOptions": {
    "ecmaVersion": 2020,
    "sourceType": "module"
  },
  "rules": {
    "@typescript-eslint/no-var-requires": "off",
    "@typescript-eslint/no-unused-vars": "off",
    "prettier/prettier": [
      "error",
      {
        "trailingComma": "all",
        "endOfLine": "lf",
        "semi": false,
        "singleQuote": true,
        "printWidth": 80,
        "tabWidth": 2
      }
    ]
  }
}
```

##起動

expressの起動

```
$ cd docker-dir
$ docker-compose up -d // コンテナ起動時にnpm startが実行され、nodemonで変更監視される
```

テストの際

```
$ cd docker-dir
$ docker-compose up mysql phpmyadmin -d
$ cd ../api-dir
$ npm run db // DBのセットアップ
$ npm run test
```

## 終わりに
今回、フロントエンドはVue.js（TypeScript）、APIはexpress（TypeScript）を採用した結果、
フロントエンジニアにもAPI製造を手伝ってもらうことができた。
言語の壁はあまり感じることなく、スムーズに作業を進められている様子だった。