Docker+nginx+Express(Node.js)で簡易なAPIサーバーを作る

# 概要
この記事は「[【マイスター・ギルド】本物のAdvent Calendar 2022](https://adventar.org/calendars/7794)」9日目の記事です。

まぁかくかくしかじかありまして、JavaScriptの練習としてAPIを叩いて返ってきた値(JSON)を画面に表示してみよう、そのための環境を作ろうってなったんです。
JavaScriptの練習だからフロントはもちろん使うけど、API側もNodeで書けばより理解も深まるだろうということでExpressなるフレームワークがあると知ってそれを使って環境構築をしてみました。
最近nginxの勉強もしたのでプロキシ設定を使った構成の練習台にもちょうどよかったです。




# 環境を作ろう
## 準備
こんな感じのディレクトリ構成にします。今回はデスクトップに作成しました。
```text:ディレクトリ構成
js-practice
├─ backend
├─ docker
│  ├─ nginx
│  └─ node
└─ frontend
```


## nginxの設定
nginxで設定するところは静的ファイルの参照場所とリバースプロキシです。

まずはこの記事を参考にnginxの`default.conf`ファイルを取り出します(実は自分の記事)。

https://qiita.com/igossou/items/f42e3e8b23547a6c8b02


以下コマンドでnginxのコンテナを立ち上げてコンテナ内に入ります。
```bash:コマンドライン
docker run --rm -v /$PWD/docker/nginx:/tmp -it nginx bin/bash
```
マウントした`/tmp`に`default.conf`ファイルをコピーしてホスト側に取り出します。
```bash:コマンドライン
cp /etc/nginx/conf.d/default.conf /tmp/
exit
```
これで`docker/nginx`配下に`default.conf`が入りました。
で、中身を以下のように書き換えます。
```conf:docker/nginx/default.conf
server {
    listen       80;
    server_name  localhost;

    root   /usr/share/nginx/html/public;
    index  index.html index.htm;

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    location /api/ {
        proxy_pass http://node:9000;
    }
}
```
`root   /usr/share/nginx/html/public;`の部分がデフォルトで参照されるディレクトリですね。
`proxy_pass http://node:9000;`の部分がプロキシの設定です。

またプロキシする場合、バックエンドへのリクエストはnginxから行われることになるためブラウザのリクエストに含まれるヘッダー情報は失われてしまいます。
ブラウザからのヘッダー情報をバックエンドでも参照する場合は`proxy_set_header`を使って設定します。
今回は特に設定しませんがブラウザからのヘッダー情報を使用する場合は気をつけてください。
```conf:default.conf
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
```
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers#%E3%83%97%E3%83%AD%E3%82%AD%E3%82%B7%E3%83%BC

## Expressの設定
Expressでは`Hello,Express!`をJSON形式で返すAPIを作ってみます。
ちなみにExpressのことはよく知りません😊

まずはbackendディレクトリに移動して`package.json`を用意してからExpressをインストールします。
```bash:コマンドライン
cd backend
npm init -y
npm install express
```

インストールできたら`app.js`というファイルを作成してAPIを作ってみます。
```javascript:backend/app.js
const express = require('express');
const app = express();
const port = 9000;

app.get('/api/v1/', (req, res) => {
  setTimeout(() => {
    res.json({
      message: 'Hello,Express!'
    });
  }, 500);
});

app.listen(port, () => {
  console.log(`listening on port: ${port}`);
});
```
https://expressjs.com/en/starter/hello-world.html

Expressの公式を少しいじって500ms後にJSON形式で`{message: Hello,Express!}`を返します。
(`setTimeout`関数を使っているのはAPIらしくレスポンスが遅れてくるようにしてみただけで必要な処理ではありません)
ここで一つ問題があって今後`app.js`を書き換えるたびにサーバー再起動が必要になります。
サーバー再起動するためにnodeのプロセスをkillしたいのですがnodeコンテナはこれが一筋縄でいかず結局Dockerコンテナの再起動をするしかないようです。
これでは不便なので`app.js`に変更があればそれを検知して再起動してくれる`nodemon`を入れます。
```bash:コマンドライン
npm install -D nodemon
```

https://www.npmjs.com/package/nodemon

mainで読み込むファイルは`app.js`なので
`nodemon`を動かすスクリプトと一緒に`package.json`を以下のように変更します。
```json:backend/package.json
___略___
  "main": "app.js",
  "scripts": {
    "start": "nodemon /app"
  },
___略___
```


## Dockerの設定
Dockerの設定ではnginxのWebサーバーとnodeのAPIサーバーをそれぞれ立てます。
まずはそれぞれの`Dockerfile`を作成します。
```docker:docker/nginx/Dockerfile
FROM nginx:latest
```
```docker:docker/node/Dockerfile
FROM node:16-alpine

WORKDIR /app

COPY ./backend ./

EXPOSE 9000
CMD ["npm", "start"]
```
nginxはDockerfile作らなくても`docker-compose.yml`に収まるんですが、個人的な好みで`Dockerfile`に書き出してます。
次にroot配下に`docker-compose.yml`をつらつらと書きます。
```yaml:docker-compose.yml
version: '3.9'

services:
  nginx:
    container_name: nginx
    build:
      context: .
      dockerfile: ./docker/nginx/Dockerfile
    ports:
      - 80:80
    volumes:
      - ./frontend/public:/usr/share/nginx/html/public
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf

  node:
    container_name: node
    build:
      context: .
      dockerfile: ./docker/node/Dockerfile
    ports:
      - 9000:9000
    volumes:
      - ./backend:/app
    tty: true
```

## フロントエンドの設定
フロントエンドと言いつつそれほど大層なものは作らずとりあえず`index.html`を準備します。
`frontend/public`ディレクトリの中に`index.html`を作成します。
```html:frontend/public/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <h1>Hello,World!</h1>
</body>
</html>
```

## ディレクトリ構成確認
ここまでの環境構築でこんな形になってればOKです。
```text:ディレクトリ構成
js-practice
├─ backend
│  ├─ node_modules
│  ├─ app.js
│  ├─ package-lock.json
│  └─ package.json
├─ docker
│  ├─ nginx
│  │  ├─ default.conf
│  │  └─ Dockerfile
│  └─ node
│     └─ Dockerfile
├─ frontend
│  └─ public
│     └─ index.html
└─ docker-compose.yml
```


# 動かしてみよう
では準備ができたので、動かしてみましょう。
`js-practice`ディレクトリ直下に移動してから満を持して`docker-compose up`。
```bash:コマンドライン
docker-compose up
```

## 静的ファイルの確認
[http://localhost]()にアクセスしてみて下記画面ならOK、nginxに静的ファイルが正しく配置されています。
![スクリーンショット 2022-11-21 22.47.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627595/a1cb8c42-8b69-a54f-2b10-0356cec4b0ab.png)

## APIの確認
[http:localhost:9000/api/v1/]()にアクセスしてみて下記画面ならOK、ExpressのAPIサーバーが正しく立ち上がっています。
![スクリーンショット 2022-11-21 22.49.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627595/2ed807f8-8864-d3a2-4d93-f8c9ddde9059.png)


## リバースプロキシの確認
[http:localhost/api/v1/]()にアクセスしてみて下記画面ならOK、nginxの設定により`/api/`のリクエストがAPIサーバーへ正しくプロキシしています。
![スクリーンショット 2022-11-21 22.49.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627595/36b5c52a-a497-8649-1440-beaa97f3f04c.png)


# APIの結果を取得しよう
## APIを叩く準備
フロントエンドからAPIをコールして結果を表示させてみます。
まずは`axios`を入れたいのでフロント側でも`package.json`を作ります。
```bash:コマンドライン
cd frontend
npm init -y
npm install axios
```
https://www.npmjs.com/package/axios


あと`axios`をimportして使うのにwebpackに代わってlaravel-mixを使います。
```bash:コマンドライン
npm install -D laravel-mix
```
https://laravel-mix.com/docs/6.0/installation


`frontend`ディレクトリ直下に`webpack.mix.js`を作成して以下のようにします。
```javascript:frontend/webpack.mix.js
const mix = require('laravel-mix');

mix.disableNotifications(); // windows環境だと通知が邪魔なので通知OFF
mix.js('src/js/main.js', 'public/js/main.js');
```
`src/js/main.js`をコンパイルして`public/js/main.js`に出力しなさいということです。
(`src/js/main.js`は後で作ります)
フロント側の`package.json`の`script`も編集します。
```json-doc:frontend/package.json
___略___
  "scripts": {
    "build": "mix",
    "watch": "mix watch"
  },
___略___
```
これでひとまず準備はOK。

ちなみに`laravel-mix`の使い方はこの記事でも書いたので参考までに(また自分の記事)。

https://qiita.com/igossou/items/6c0edf083bf491c63201

## APIを叩いてみる
ボタンを押したらgetリクエストのAPIを叩いて、レスポンスを表示するということをやってみます。
レスポンスは`backend/app.js`で定義したJSONですね。
```json-doc:JSONレスポンス
{
  message: "Hello,Express!"
}
```

`index.html`を更新して`src/js/main.js`を`frontend`ディレクトリ配下に新たに作成します。
```html:frontend/public/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <h1 id="message">Hello,World!</h1>
  <button id="btn">Message取得</button>
  <script src="./js/main.js"></script>
</body>
</html>
```
```javascript:frontend/src/js/main.js
import axios from "axios";

const msg = document.getElementById('message');
const btn = document.getElementById('btn');

const getMessage = async () => {
  const response = await axios.get('/api/v1/');
  return response.data.message;
}

btn.addEventListener('click', async () => {
  await getMessage()
    .then(res => msg.textContent = res)
    .catch(err => console.log(err));
});
```

そして`frontend/src/js/main.js`をコンパイルします。
```bash:コマンドライン
npm run watch
```

[http://localhost]()はこんな画面になりました。
![スクリーンショット 2022-11-21 23.53.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627595/7a74205b-549f-f9a7-f22b-ae6e4d2c4cf1.png)

ボタンを押すと...
![スクリーンショット 2022-11-22 0.01.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627595/dc573620-9b9c-0bce-f7f4-8c4edb4de00d.png)

レスポンスを受け取ってその内容に切り替わりました〜。


# まとめ
ちょっとしたきっかけがあって、この環境を作ることになりましたがdocker内のnginxのリバプロの設定で`http://localhost:9000`ではうまくいかないことや`nodemon`や`express`といったツールやフレームワークを知ることができました。(host側のlocalhostとdocker側のlocalhostは違いますよということらしいです)
```
location /api/ {
  # OK
  proxy_pass http://node:9000; 
  # NG
  # proxy_pass http://localhost:9000;
}
```

小さな学びがいくつかあったのでよかったです。
これを作った後にフロントをReactに変えてみたりもしました。
環境構築もこなれてきた感があります(嘘)。

今回はシンプルなリクエストを送ってレスポンスを受け取るだけですが、DBを追加してみたりすればよりいい感じのアプリが作れるようになると思います。
フロントとバックのJavaScriptを勉強するための簡易な環境構築でした。

最後にディレクトリ構成はこんな感じになります。
```text:ディレクトリ構成
js-practice
├─ backend
│  ├─ node_modules
│  ├─ app.js
│  ├─ package-lock.json
│  └─ package.json
├─ docker
│  ├─ nginx
│  │  ├─ default.conf
│  │  └─ Dockerfile
│  └─ node
│     └─ Dockerfile
├─ frontend
│  ├─ node_modules
│  ├─ public
│  │  ├─ js/main.js
│  │  └─ index.html
│  ├─ src
│  │  └─ js/main.js
│  ├─ mix-manifest.json
│  ├─ package-lock.json
│  ├─ package.json
│  └─ webpack.mix.js
└─ docker-compose.yml
```