Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Express編~

## 内容
Docker上でExpress APIサーバーコンテナを構築しシンプルなレスポンスを返すまでの環境を構築していきます。

前回の記事まででアプリケーションコンテナ / DBコンテナを作成しましたが、今回はそれらを繋ぎ合わす役目のAPIサーバーコンテナを構築し実際にデータをフロントに渡すまでを行っていきます。

サーバー側はボリュームが多いため、Expressの基本構築とSequelizeの導入の記事を分けております。

[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Vue編~](https://qiita.com/WEBERYOTA/items/b686478ec0c8c9a6bddd)
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~MySQL編~](https://qiita.com/WEBERYOTA/items/6a5ef7bdfc5bd972f92b)

## 作業手順
1. APIコンテナ作成
2. ts-node/nodemonの導入
3. Expressサーバー構築
4. ルーティング設定
5. APIハンドラーの設定
6. APIのテンプレートを作成

##1. APIコンテナ作成
### docker-compose.yml

```yml
version: "3"
services:
  app:
    container_name: app_container
    build: ./docker/app
    ports:
      - 8080:8080
    volumes:
      - ./app:/app
    stdin_open: true
    tty: true
    environment:
      TZ: Asia/Tokyo
    command: yarn serve
# ここから追加
  api:
    container_name: api_container
    build: ./docker/api
    ports:
      - 3000:3000
    volumes:
      - ./api:/api
    tty: true
    environment:
      CHOKIDAR_USEPOLLING: 1
      TZ: Asia/Tokyo
    depends_on:
      - db
# ここまで追加
  db:
    container_name: db_container
    build: ./docker/db
    image: mysql:5.7
    ports:
      - 3306:3306
    volumes:
      - ./db/conf/my.cnf:/etc/mysql/conf.d/mysql.cnf
      - ./db/init_db:/docker-entrypoint-initdb.d
      - test_data:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - TZ="Asia/Tokyo"

volumes:
  test_data:
```

### docker/api/Dockerfile
```
FROM node:12.13-alpine

ENV NODE_PATH /usr/local/lib/node_modules

WORKDIR /api
```

### 起動確認

```
$ docker-compose up -d
$ docker ps
```
api_container が起動してればOK！

### apiコンテナアクセス
```
$ docker exec -it api_container sh
```

## 2. ts-node/nodemonの導入
### 必要なライブラリを導入

```
$ yarn init
$ yarn add --dev typescript ts-node prettier　@types/node
```

### .prettierrc

```
{
  "singleQuote": true,
  "semi": false
}
```

### tsconfig init

```
$ ./node_modules/.bin/tsc --init
```
tsconfig.jsonが生成されるのでこちらをベースにアプリを構築していきます。

### tsconfig.json

```json
  "include": ["./**/*"] //末尾に追加
```

### package.json

```json
"scripts": {
	"ts-node": "ts-node  index.ts"
},
```
ts-node でTypeScriptファイルを実行できるようにスクリプトを設定しておきます。

### api/index.ts

```ts
const message: string = 'テスト'
console.log(message)
```
TypeScriptを実行するためのメインファイルを作成します。
ts-nodeが正常に動作するかチェックするためのダミーを入力します。

テスト

```
$ yarn ts-node
```
![スクリーンショット 2020-09-21 19.43.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427188/d5ff7180-4aac-46d1-c74f-038b07ae2c8c.png)

TypeScriptがコンパイルされ、上記のメッセージが表示されればOKです！

### nodemon 導入
ファイルに変更があった際に自動ビルドを行ってくれるライブラリを導入します。

```
$ yarn add --dev nodemon
```

nodemon.json

```json
{
  "watch": ["api/**/*"],
  "ext": "ts",
  "exec": "ts-node ./index.ts"
}
```
nodemonの設定ファイルを作成します。
ts-nodeをnodemon経由で実行し、apiディレクトリ直下を全て監視対象に設定します。

package.json

```
  "scripts": {
    "tsc": "tsc",
    "ts-node": "ts-node  index.ts",
    "nodemon": "nodemon"
  },
```

### docker-compose.yml
```yml
  api:
    container_name: api_container
    build: ./docker/api
    ports:
      - 3000:3000
    volumes:
      - ./api:/api
    tty: true
    environment:
      CHOKIDAR_USEPOLLING: 1
      TZ: Asia/Tokyo
    depends_on:
      - db
    command: yarn nodemon #こいつを追加
```
コンテナ起動時にnodemon経由で自動で立ち上がるようコマンドを追加しておきます。

##3. Expressサーバー構築
ここまででTypeScriptの実行と変更時の自動ビルドの設定が完了しました。
ここから実際のAPIサーバーとなるExpressを構築していきます。

```
$ yarn add express
$ yarn add --dev @types/express
```

### api/index.ts

```typescript

import express from 'express'

const app = express()
const port = 3000

app.get('/', (req, res) => res.send('Test Express!'))

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

### 起動確認

```
$ exit                    //コンテナから抜ける
$ docker-compose stop     // コンテナを停止
$ docker-compose up -d    //コンテナ再起動
```

localhost:3000 にアクセスして、「Test Express!」と表示されていれば問題なくレスポンスが返っている状態！
念のためindex.tsの「Test Express!」を別のメッセージを変更し、再度 localhost:3000 にアクセスした時に、メッセージが変わるか確認しましょう。自動で変わっていれば nodemon が適切に動き自動ビルドがされている状態です！

##4. ルーティング設定
今はまだ localhost:3000 でしかアクセスできない状態ですが、下記のようにURLに応じたAPIをコールしたいのでルーティングの設定を行っていきます。

例)

- localhost:3000/api/**tests**  
    - testテーブルのデータを全件取得
- localhost:3000/api/**tests/1**
    - testテーブルからIDが1のデータを取得
- localhost:3000/api/**users**
    - userテーブルからデータを全件取得

### api/routes/index.ts

```ts

import Express from 'express'

const router = Express.Router()

export default router
```
### api/routes/tests/testsController.ts

```typescript

import { Router } from 'express'

const router = Router()

export default router
```

下処理はここまでで、実際にルーティングを指定していきます。
今回は「localhost:3000/api/tests」でアクセスできるように変更

### api/routes/index.ts

```ts
import Express from 'express'

const router = Express.Router()

router.get('/', (req, res, next) => {
  res.send('テスト')
})

export default router
```

### api/index.ts

```typescript
import express from 'express'
import router from './routes/index'

const app = express()
const port = 3000

app.use('/api', router)

app.listen(port, () => console.log(`Example app listening on port ${port}!`))
```
上記で設定した[app.use('/api', router)]こちらの設定で localhost:3000/api が渡ってきた時に routes/index.ts のルーティングへと移動する流れになります。
実際に動作チェックしてみましょう。

localhost:3000/api でアクセス
**テスト**が表示されればOK!

実際のアプリでは複数のDBへのアクセスの際にエンドポイントを分岐する必要があるので、下記のように枝分かれするようにルーティングを設定します。

localhost:3000/api/**{この値に応じて分岐}**

### api/routes/tests/testsController.ts

```ts

import { Router } from 'express'

const router = Router()

router.get('/', (req, res, next) => {
  res.send('ルーティング完了!')
})

export default router
```

### api/routes/index.ts

```ts
import Express from 'express'
import tests from './tests/testsController'

const router = Express.Router()

router.use('/tests', tests)

export default router
```
### 確認！
localhost:3000/api/tests へアクセス
「ルーティング完了！」が表示されればOK！

ここまででルーティングの設定は完了です！

### おさらい
1. api/index.ts
    - localhost:3000/api をキャッチして routes/index.ts にパス
2. api/routes/index.ts
    - /tests をキャッチして tests/testsController.ts にパス
3. api/routes/tests/testsController.ts
    - 末尾が無い場合にレスポンスを返す

## 5. APIハンドラーの設定
ついでに、正常なレスポンスが返るパターンとエラー時のレスポンスを切り替えるハンドラーを作成しておきます。

### api/constants/error.ts

```ts
export type ErrorCode = {
  status: number
  type: string
  message: string
}

/**
 * パラメーターが誤っている場合のエラー
 */
export const PARAMETER_INVALID: ErrorCode = {
  status: 400,
  type: 'PARAMETER_INVALID',
  message: 'The parameter is invalid.',
}

/**
 * データが存在しない場合のエラー
 */
export const NO_DATA_EXISTS: ErrorCode = {
  status: 400,
  type: 'NO_DATA_EXISTS',
  message: 'No data exists.',
}
```
エラーパターンが増えた際はこちらにエラーコードを増やしていきます。

### api/core/handler.ts

```ts
import { Request, Response } from 'express'
import { ErrorCode } from '../constants/error'

/**
 * APIのハンドリングをする機能
 */
export class Handler {
  constructor(private req: Request, private res: Response) {}

  /**
   * データの送信
   */
  json<T>(data: T): void {
    this.res.json({ data: data })
  }

  /**
   * エラーの送信
   */
  error(error: ErrorCode): void {
    this.res.status(error.status).send({ error: error })
  }
}
```

APIに異常が発生した際はエラー出力に分岐し、正常な場合はjson形式でリターン

## 6. APIのテンプレートを作成
localhost:3000/api/test でアクセスした際に tests テーブルからデータを全件取得するAPIのテンプレートを作成しておきます。

### api/routes/tests/get_tests.ts

```ts
import { Request, Response } from 'express'
import { Handler } from '../../core/handler'

export class GetTests {
  handler: Handler

  constructor(req: Request, res: Response) {
    this.handler = new Handler(req, res)
  }

  /**
   * メイン処理
   */
  async main() {
    const data = this.getTests()

    return this.handler.json(data)
  }

  /**
   * メッセージを返す
   */
  getTests() {
    const message = 'get_tests実行'

    return message
  }
}
```
こちらが実際に呼ばれるAPI

### api/routes/tests/testsController.ts

```ts
import { Router } from 'express'
import { GetTests } from '../tests/get_tests' // 上記のAPIをインポート

const router = Router()

router.get('/', (req, res, next) => {
  new GetTests(req, res).main().catch(next) // 変更
})

export default router
```
### 確認！
localhost:3000/api/tests
![スクリーンショット 2020-09-22 0.58.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427188/27d65554-0f9d-40a3-7cac-1f75cf9c85e6.png)

こんな感じでJSON形式で返ってくればOK！

## まとめ
お疲れ様でした！
ここまでで アプリケーションコンテナ、DBコンテナ、APIサーバーコンテナができました。
次回からはそれぞれのコンテナをつなげて実際にDBデータをフロントに渡して描写するまでを行っていこうと思います！

[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Vue編~](https://qiita.com/drafts/b686478ec0c8c9a6bddd/edit)
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~MySQL編~](https://qiita.com/WEBERYOTA/items/6a5ef7bdfc5bd972f92b)
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Express編~](https://qiita.com/WEBERYOTA/items/649cacbfb1623079b090)
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Sequelize編~
](https://qiita.com/WEBERYOTA/items/e595176890cb0f435014)