Dockerで簡易版Aidemyをつくってみよう (その3) ~Expressによるバックエンド~

# 概要
Aidemy アドベントカレンダー17日目担当の @km42428 です。
今回は、Dockerで簡易版Aidemyをつくってみようシリーズのその3です。
前回はフロントエンドを作成したので、今回はバックエンドを作成します。

ちなみに、前回までのリンクは以下です。

Dockerで簡易版Aidemyをつくってみよう (その1) ~全体構成とDockerのセットアップ~
https://qiita.com/km42428/items/4e4653aa0e813282986b

Dockerで簡易版Aidemyをつくってみよう (その2) ~React.jsによるフロントエンド~
https://qiita.com/km42428/items/989dac5e7450501c452d

# 今回の目標
今回はエクササイズデータを渡すAPIを作成し、その２で作成したWebページから呼び出して値を表示して見ましょう。
そして、前回同様 `docker-compose` コマンドを利用し、フロントエンド・バックエンドを一発で起動できるようにするのが目標です。
バックエンドはNode.jsのバックエンドライブラリである `Express` を使います。
ソースは以下に公開しています。
https://github.com/km42428/aidemy-simple-pack/tree/backend

今回の記事は `backend` ブランチで動作しています。
`master`ブランチは適宜更新しているので、この記事と同じ動作をさせたい場合は、
以下のコマンドを入力してください。
※`自分の作業ブランチ`は適宜書き換えてください。

```shell
# クローン
$ git clone https://github.com/km42428/aidemy-simple-pack.git 

# リモートのfrontendブランチにチェックアウト
$ git fetch && git checkout origin/backend

# ローカルの自分の作業ブランチにチェックアウト
$ git checkout -b {自分の作業ブランチ} 
```


# APIの構成

APIは以下のような構成を考えます。

| メソッド | パス | 内容 |
|:-----------:|:------------|:------------|
| GET | / | 生存確認 ({}を返すのみ) |
| GET | /exercises | エクササイズ全てを取得する |
| GET | /exercises/:exerciseId | 特定の一つのエクササイズを取得する |

# 事前準備
バックエンドの挙動をローカルで確認するには、
`node` と `npm` を事前にインストールする必要があります。
インストール手順については、前の記事にまとめているのでそちらをご覧ください。
https://qiita.com/km42428/items/989dac5e7450501c452d#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99

※ 今回の最終目的も`Docker`を使って環境開発不要で起動することです。
なので、環境開発なんてしたくないやいっ！という方は、
ソースをcloneした上で、<a href="#dockerによる操作" target="_blank">Dockerによる操作</a>からお読みください。

# ローカルのでの開発

## バックエンドフォルダを作成・移動

前回記事のフロントエンドと合わせて挙動を確認したい場合は、
前回の`aidemy-frontend` フォルダと同じ階層で作業してください。
バックエンドのフォルダ名は `aidemy-backend` とします。

```
mkdir aidemy-backend && cd $_
```

後ろの `cd $_` で作成したフォルダに移動できます。

### npm の有効化を行う
以下のコマンドで npm コマンドの初期化を行います。

```
npm init
```

以下のような設問が出てきますが、全てEnterでOKです

```
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (aidemy-backend)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /Users/XXXXXXXX/aidemy-simple-pack/aidemy-backend/package.json:

{
  "name": "aidemy-backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this ok? (yes) 
```

実行が終わると、
package.jsonが生成されています

## .gitignore の作成
`npm init` が終わると、 `node_modules` というフォルダが作成されます。
こちらはnpmで用いるモジュール等が大量に入っています。
モジュールの情報は package.jsonのdependencies にも入っていて、
もし他の人とソースをgitで共有する場合、 `npm install` コマンドを入力すると同様の `node_modules` が作成されます。
なので、このフォルダ自体は共有不要なのでgitで履歴に反映されないように `.gitignore` ファイルを作成します。

`aidemy-backend` 直下に`.gitignore`を作成します

```
touch .gitignore
```

`.gitignore`を編集します。

```
node_modules/
```

## 必要なモジュールのインストール

```
npm install express body-parser
```

## APIの作成
`aidemy-backend` 直下にindex.jsを作成します

```
touch index.js
```

実際にAPIを記述していきましょう。

```javascript
// 必要なパッケージの読み込み
var express = require("express");
var app = express();
var bodyParser = require("body-parser");
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept"
  );
  res.header(
    "Access-Control-Allow-Methods",
    "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"
  );
  next();
});

// expressでAPIサーバを使うための準備
var router = express.Router();

// エクササイズマスターデータ
const exercises = [
  {
    exerciseId: "exercise1",
    title: "1. Hello world",
    script: "# Hello worldを出力しましょう\n",
    answer: "# Hello worldを出力しましょう\nprint('Hello world')"
  },
  {
    exerciseId: "exercise2",
    title: "2. コメントの入力",
    script: "# 3 + 5 の結果を出力しましょう\n",
    answer: "# 3 + 5 の結果を出力しましょう\nprint(3 + 5)"
  }
];

/**
 * 特定のエクササイズを抽出する関数
 * @param exerciseId - エクササイズID
 * */
function getExercise(exerciseId) {
  return exercises.find(el => {
    return el.exerciseId === exerciseId;
  });
}

/* -------- 以下にAPIを記述 -------- */

router.route("/").get(async function(_req, res) {
  res.json({});
});
router.route("/exercises").get(async function(_req, res) {
  if (!exercises) {
    res.status(404).json({
      code: 404,
      msg: "Exercises not found"
    });
  }
  res.json(exercises);
});
router.route("/exercises/:exerciseId").get(async function(req, res) {
  console.log(req.params);
  const exercise = getExercise(req.params.exerciseId);
  if (!exercise) {
    res.status(404).json({
      code: 404,
      msg: "Exercise not found"
    });
  }
  res.json(exercise);
});

/* -------- 以上にAPIを記述 -------- */

// ルーティング登録
app.use("/v1", router);

// port4000番で出力を受ける
app.listen(4000);


```


## package.jsonの編集

package.jsonを１行加えます。

```diff

{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
+   "serve": "node index.js"
  }
}
```

## 動作確認

### 起動
以下のコマンドで起動します。

```
npm run serve
```

### 出力確認
こちらに、動作確認に便利な `Postman` についてまとめられた記事を紹介します。
https://qiita.com/zaburo/items/16ac4189d0d1c35e26d1
かなり便利なので入れておきましょう。

以下の画像のように、GETメソッドで右の欄にURLを書いてSendを押せば、APIの動作確認ができます。
<img width="1679" alt="スクリーンショット 2018-12-17 11.54.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/291512/7bcf1ddc-fb37-fb00-957a-de2f2d984afb.png">

GETメソッドであれば、ブラウザのURL欄に貼り付けても動きます。

URL

```
http://localhost:4000/v1/
```
出力

```json

{}
```

URL

```
http://localhost:4000/v1/exercises
```

出力

```json

[
  {
    "exerciseId": "exercise1",
    "title": "1. Hello world",
    "script": "# Hello worldを出力しましょう\n",
    "answer": "# Hello worldを出力しましょう\nprint('Hello world')"
  },
  {
    "exerciseId": "exercise2",
    "title": "2. コメントの入力",
    "script": "# 3 + 5 の結果を出力しましょう\n",
    "answer": "# 3 + 5 の結果を出力しましょう\nprint(3 + 5)"
  }
]
```

URL

```
http://localhost:4000/v1/exercises/exercise1
```

出力

```json


{
  "exerciseId": "exercise1",
  "title": "1. Hello world",
  "script": "# Hello worldを出力しましょう\n",
  "answer": "# Hello worldを出力しましょう\nprint('Hello world')"
}
```

URL

```
http://localhost:4000/v1/exercises/wrongid
```

出力

```json


{
  "code": 404,
  "msg": "Exercise not found"
}
```

# フロントエンドの設定
<a href="https://qiita.com/km42428/items/989dac5e7450501c452d" target="_blank">前回の記事</a>ではエクササイズ情報を `aidemy-frontend`内に記述していました。
これを、今作成した `aidemy-backend` から参照するようにします。

## 必要なモジュールをinstallする
今回は `axios` というモジュールを使います。
`axios` はnode.jsでhttpリクエストを行うツールです。
公式ドキュメントは以下です。
https://github.com/axios/axios

```shell

npm install axios
```

## axiosでAPIを叩く準備
axiosをimportし、apiのurlも変数に入れます。

```javascript

  import React, { Component } from "react";
  import { BrowserRouter, Route, Link } from "react-router-dom";
  import AceEditor from "react-ace";
  import "brace/mode/python";
  import "brace/snippets/python";
  import "brace/theme/tomorrow";
+ import axios from "axios";

+ const apiUrl = "http://localhost:4000/v1";
```

## フロントエンドのマスターデータをコメントアウト
バックエンドからエクササイズ情報を取得するので、フロントエンドの記述はコメントアウトします。
複数行コメントアウトは `/* */` で囲めば大丈夫です。

```javascript

/*
// マスターデータ
const exercises = [
  {
    exerciseId: "exercise1",
    title: "1. Hello world",
    script: "# Hello worldを出力しましょう\n"
  },
  {
    exerciseId: "exercise2",
    title: "2. コメントの入力",
    script: "# 3 + 5 の結果を出力しましょう\n"
  }
];

function getExercise(exerciseId) {
  return exercises.find(el => {
    return el.exerciseId === exerciseId;
  });
}
*/
```

## エクササイズ情報をAPI取得する
`axios` を使って、API経由でエクササイズを取得します。

```diff

// exerciseの一覧
class Exercises extends Component {
  constructor() {
    super();
    this.state = {
      exercises: []
    };
  }

  // コンポーネントのレンダリングが終了したら呼び出される
- componentDidMount() {
-   this.setState({
-     exercises
-   });
- }
+ async componentDidMount() {
+   const exercises = (await axios.get(`${apiUrl}/exercises/`)).data;
+   // APIにアクセスできない場合に備えて条件分岐
+   if (!exercises) {
+     this.setState({
+       exercises: []
+     });
+     return;
+   }
+   this.setState({
+     exercises
+   });
+ }

  // 実際の表示部分
  render() {
    ...
  }
}

// 特定のexerciseの演習部分
class Exercise extends Component {
  constructor() {
    super();
    this.state = {
      exerciseId: "",
      title: "",
      script: ""
    };
    this.onChange = this.onChange.bind(this); // はじめにonChangeをthisにbindしておく
  }

  // コンポーネントのレンダリングが終了したら呼び出される
  async componentDidMount() {
-   const exercise = getExercise(this.props.match.params.id);
+   const exercise = (await axios.get(`${apiUrl}/exercises/${this.props.match.params.id}`)).data;
    if (exercise) {
      this.setState({
        exerciseId: exercise.exerciseId,
        title: exercise.title,
        script: exercise.script
      });
      return;
    }
    this.setState({
      exerciseId: "",
      title: "",
      script: ""
    });
  }
  ...
```

これでWEBページに値が表示されるはずです。

`aidemy-frontend`, `aidemy-backend` どちらも起動させて挙動を見てみてください。

Chromeを使っている場合は、右クリックで検証 > Network より、APIから値を取得しているのを確認できます。


# Dockerによる操作

## docker-compose で動作するために
### aidemy-backendフォルダ内にDockerfileを配置

```
　├ aidemy-backend/
　│　└ Dockerfile # 新規追加
　└ aidemy-frontend/
　　　　└ Dockerfile
```

Dockerfileを記述

```dockerfile

FROM node:8
WORKDIR /usr/src/app
COPY . .
RUN npm install
CMD npm run serve
```


### aidemy-backendフォルダと同じ階層にdocker-compose.yml 配置

```
　├ aidemy-backend/
　│　└ Dockerfile
　├ aidemy-frontend/
　│　└ Dockerfile
　└ docker-compose.yml
```

docker-compose.ymlを記述


```yaml


version: "3"
services:
  aidemy-frontend:
    build: ./aidemy-frontend
    image: aidemy-frontend
    environment:
      - ENV
    ports:
      - 5000:5000
  aidemy-backend:
    build: ./aidemy-backend
    image: aidemy-backend
    environment:
      - ENV
    ports:
      - 4000:4000
```

## docker-composeによる起動
前回同様にdocker-composeで起動します

### docker imageの作成

```
docker-compose build
```

### フォアグラウンドでの起動
```
docker-compose up
```

http://localhost:8080 で動作すればOK

### ※バックグラウンドでの起動

```
docker-compose up -d
```

### ※バックグラウンドで起動したプロセスの終了

```
docker-compose down
```


今回は以上になります。
Dockerの使い方には慣れてきたでしょうか？
次回は12/19にデータベースを開発していきます！