Nuxt.js + Rails(API) on DockerのHello Worldするべ！

# Goal
友人からNuxt.jsいいよーと言われたのでひとまず触ってみるかというノリでNuxt.js + Rails(API) on DockerのHello Worldをめざします！

この記事のゴールはDocker上でフロントエンドとしてNuxt.js、バックエンドとしてRailsが連携しあってRailsのscaffold的にUserのCRUDができることです。
図にすると以下のような感じです。

![Untitled Diagram (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/46e524b3-46ad-38b2-7a31-38f74274bdab.png)

# Table of contents
1. Dockerコンテナの準備
2. Nuxt.jsのHello world
3. Rails(API)のHello world
4. Nuxt.js + Rails(API)のHello world

Dockerやdocker-composeはすでにインストール済みの前提でいきます！

# 1. Dockerコンテナの準備
まずはNuxt.jsやRailsをDocker上で動作させるためのファイルの準備をしていきます。
Nuxt.jsは「[docker で nuxt.js を開発環境を建てるだけ - Qiita](https://qiita.com/kitsuki00/items/ed51dbb254bcc6c94fbd)」を参考にさせていただきました。
Rails(API)については「[Rails on Docker（alpine）でAPIコンテナをつくってみた - Qiita](https://qiita.com/at-946/items/70a5bd3ece095ead584a)」にて記事投稿しております。

詳細については各記事をご参考いただければ幸いですが、最終的なアウトプットは以下のようなものです。

```text:DirectoryStructure
/
|--front/
|    |--Dockerfile
|--back/
|    |--Dockerfile
|    |--Gemfile
|    |--Gemfile.lock #空ファイル
|--docker-compose.yml
```

```dockerfile:front/Dockerfile
FROM node:12.5.0-alpine

ENV HOME="/app" \
    LANG=C.UTF-8 \
    TZ=Asia/Tokyo 

WORKDIR ${HOME}

RUN apk update && \
    apk upgrade && \
    npm install -g npm && \
    npm install -g @vue/cli

ENV HOST 0.0.0.0
EXPOSE 3000
```

```dockerfile:back/Dockerfile
FROM ruby:2.6.3-alpine3.10

ENV RUNTIME_PACKAGES="linux-headers libxml2-dev make gcc libc-dev nodejs tzdata postgresql-dev postgresql git" \
    DEV_PACKAGES="build-base curl-dev" \
    HOME="/app" \
    LANG=C.UTF-8 \
    TZ=Asia/Tokyo

WORKDIR ${HOME}

ADD Gemfile ${HOME}/Gemfile
ADD Gemfile.lock ${HOME}/Gemfile.lock

RUN apk update && \
    apk upgrade && \
    apk add --update --no-cache ${RUNTIME_PACKAGES} && \
    apk add --update --virtual build-dependencies --no-cache ${DEV_PACKAGES} && \
    bundle install -j4 && \
    apk del build-dependencies

ADD . ${HOME}

CMD ["rails", "server", "-b", "0.0.0.0"]
```

```Gemfile:back/Gemfile
source 'https://rubygems.org'
gem 'rails', '~>5'
```

```yaml:docker-compose.yml
version: "3"

services:
  db:
    container_name: sample_db
    image: postgres:11.4-alpine
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - ./back/tmp/db:/var/lib/postgresql/data
  
  back:
    container_name: sample_back
    build: back/
    volumes:
      - ./back:/app
    depends_on:
      - db
    ports:
      - 3000:3000
  
  front:
    container_name: sample_front
    build: front/
    command: npm run dev
    volumes:
      - ./front:/app
    ports:
      - 8080:3000
```

ここまでできたらbuildしてimageを作成しましょう。

```
$ docker-compose build
```

# 2. Nuxt.jsのHello world
imageができあがったらまずはNuxt.jsのアプリを作っていきます。

```
$ docker-compose run --rm front npx create-nuxt-app

? Project name                   --> sample_app  # アプリ名
? Project description            --> sample_app  # アプリの説明
? Author name                    --> me          # アプリの作成者
? Choose the package manager     --> Npm
? Choose UI framework            --> None
? Choose custom server framework --> None
? Choose Nuxt.js modules         --> Axios
? Choose linting tools           --> -
? Choose test framework          --> None
? Choose rendering mode          --> Universal (SSR)
```

Nuxtアプリが作成できたらアクセスできるか確認しときます。

```
$ docker-compose up front
```

`http://localhost:8080`にアクセスして以下のようなページにアクセスできればNuxt.jsのHello world完了です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/604f9daf-3337-5ceb-c56f-9b384bdbe57f.png)

（参考）Nuxt.jsではホットリローディングというファイルの変更を自動で反映してくれる機能を有効にすることができます。`npm run dev`コマンドでホットリローディングが有効になると公式で説明されていますが何やらうまくいくときといかない時がありました...うまくいかない場合は「[IT研修でVuePress+Express+Nuxt on Dockerでシステムを作成した話 - エンジニアの卵の成長日記](http://kurowasi2525.hatenablog.com/entry/2018/04/29/201924)」を参考に以下のような設定を書き加えることでホットリロードされるようになりました。

```js:front/nuxt.config.js
export default {
// 省略
  watchers: {
    webpack: {
      poll: true
    }
  }
// 省略
}
```

# 3. Rails(API)のHello world
まずRailsアプリを作成しましょう。`--api`をオプションにつけることでAPIモードに不要なもの、例えばViewなどが含まれないようにrails newすることができます。

```
$ docker-compose run --rm back rails new . -f -d postgresql --api
```

Railsアプリが作成されたらDB接続の設定をします。

```yaml:back/config/database.yml
##### 省略
default: &default
  adapter: postgresql
  encoding: unicode
  host: db            # add
  username: postgres  # add
  password:           # add
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 
##### 省略
```

DBの設定が完了したらDBを作成します。

```
$ docker-compose build back
$ docker-compose run --rm back rails db:create
```

DBの作成が完了したら、scaffoldでAPIを作ってみます。`name`属性をもつ`User`モデルを作ります。

```
$ docker-compose run --rm back rails g scaffold user name:string
$ docker-compose run --rm back rails db:migrate
```

Hello worldとして、`test`という名前のユーザーをAPIリクエストで作成してみます。

Rails APIでどのエンドポイントに何メソッドでリクエストすればいいかは`rails routes`コマンドで調べるとわかりやすいです。

```
$ docker-compose run --rm back rails routes
Prefix Verb   URI Pattern            Controller#Action
users  GET    /users(.:format)       users#index
       POST   /users(.:format)       users#create
user   GET    /users/:id(.:format)   users#show
       PATCH  /users/:id(.:format)   users#update
       PUT    /users/:id(.:format)   users#update
       DELETE /users/:id(.:format)   users#destroy
```
詳しい説明は省きますが、HTTPメソッド、URLパターン、アクションの関係性がわかるので、
ユーザーを作成する場合は「/usersにPOSTリクエスト」、特定のIDのユーザーの情報を取得する場合は「/users/:idにGETリクエスト」ということがわかります。
これを踏まえてcurlリクエストしてユーザー作成をしてきます。

```
$ docker-compose up -d back
$ curl -X POST http://localhost:3000/users -d 'user[name]=test'
$ curl http://localhost:3000/users/1
{"id":1,"name":"test","created_at":"2019-07-04T14:40:49.443Z","updated_at":"2019-07-04T14:40:49.443Z"}
```

これでRails APIのHello world完了です！

コンテナは停止しておきましょう。

```
$ docker-compose down
```

# 4. Nuxt.js + Rails(API)のHello world
さて、実際にはNuxt.jsのコンテナからRails(API)のコンテナにリクエストを流すので、Rails(API)のポートを外部に公開する必要はありません。誰からでもAPIのリクエストを受け取ってしまう状態はセキュリティ的にもよろしくありませんのでbackコンテナの外部公開ポートを削除しておきます。

```yaml:docker-compose.yml
##### 省略
  back:
    container_name: sample_back
    build: back/
    volumes:
      - ./back:/app
    depends_on:
      - db
#   ports:        # delete
#     - 3000:3000 # delete
##### 省略
```

続いてNuxt.jsをいじっていきます。Hello worldでやりたいことは、

1. `http://localhost:8080/users/:id`にアクセスして「Hello, (User.name)」と表示させたい
2. `http://localhost:8080/users/new`にアクセスしてユーザーを追加したい

といったところにします。

## 4-1. `http://localhost:8080/users/:id`にアクセスして「Hello, (User.name)」と表示させたい
まずは`http://localhost:8080/users/:id`にアクセスした時にルーティングされるページを作成します。

```
$ mkdir front/pages/users
$ touch front/pages/users/_id.vue
```

```vue:front/pages/users/_id.vue
<template>
  <h1>Hello, {{ name }}</h1>
</template>

<script>
export default {
  asyncData({ $axios, params }) {
    return $axios.$get(`http://back:3000/users/${params.id}`)
      .then((res) => {
        return { name: res.name }
      })
  }
}
</script>
```
`$axios.$get`でGETメソッドでAPIをリクエストしています。リクエスト先は`http://back:3000/users/${params.id}`としていますが、`back`はbackコンテナを意味していますので、Railsアプリが入ったコンテナの`/user/${params.id}`にGETリクエストを飛ばしていることになります。
レスポンスの値から`name`を変数として取り出し、`template`内の`{{ name }}`に入れます。

この状態でコンテナを起動して`http://localhost:8080/users/1`にアクセスすると、Rails(API)のHello worldで作成した`test`ユーザーの情報が取得できています。
が、Backコンテナが立ち上がっていないといけない状態になったので`depends_on`しておきましょう。

```yaml:docker-compose.yml
##### 省略
  front:
    container_name: sample_front
    build: front/
    command: npm run dev
    volumes:
      - ./front:/app
    ports:
      - 8080:3000
    depends_on:
      - back
##### 省略
```

```
$ docker-compose up
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/2124254d-de07-5d41-46f5-6df44f8c7b47.png)

# 4-2. `http://localhost:8080/users/new`にアクセスしてユーザーを追加したい
続きましてユーザーの新規登録です。こちらはPOSTリクエストしてあげることで実現できます。

先ほどと同様に`http://localhost:8080/users/new`にアクセスしたときに表示されるページを作っていきます。
このページでは、Nameを入力してsubmitするとRails APIの方にPOSTリクエストを飛ばしてUserを新規登録できるようにしたいと思います。新規登録したらそのUserのHelloページ（4-1で作成）にページ遷移するようにしましょー。

まずはじめに、Rails APIにリクエストを飛ばせるようにconfigをいじっていきます。

```js:front/plugins/axios.js
export default function({ $axios, redirect }) {
    $axios.setToken('access_token')

    $axios.onResponse(config => {
        $axios.setHeader('Access-Control-Allow-Origin', 'http://back:3000')
    })
}
```

```js:front/nuxt.config.js
export default {
// 省略
  plugins: [
    'plugins/axios'
  ],
  modules: [
    '@nuxtjs/axios'
  ],
  axios: {
    proxy: true
  },
  proxy: {
    '/api/': { target: 'http://back:3000', pathRewrite: { '^/api/': '/' } }
  },
// 省略
}
```

ここらへんの設定をしないとCORSエラーってのがおきちゃう。すごくつまった。
「[Nuxt.jsのメソッド内で外部APIを叩くとcorsエラーが起きる - Qiita](https://qiita.com/naokada/items/394c1d85ad2975ba62f0)」「[nuxt.js で axios から外部APIを叩くとCORSエラーを解決 - Qiita](https://qiita.com/penton310/items/4312ddbe015476fd67ff)」の記事を参考にしました！
4-1で実施したasyncDataはSSRなので不要なようですが、通常メソッド内でリクエストをしたい場合は信頼するドメインへのリクエストのみを許可する必要があるみたいですね。

さて、上記の設定が終わったら実際にページを作っていきます。

```vue:front/pages/users/new.vue
<template>
  <section>
    <div>
      <h1>New user</h1>
      <form @submit.prevent="post">
        <label for="name">Name: </label>
        <input id="name" v-model="name" type="text" name="name" />
        <button type="submit">submit</button>
      </form>
    </div>
  </section>
</template>

<script>
export default {
  data() {
    return {
      name: ''
    }
  },
  methods: {
    post() {
      this.$axios.post(
        '/api/users',
        {
          name: this.name
        }
      ).then((res) => {
        this.$router.push(`${res.data.id}`)
      })
    }
  }
}
</script>
```
このページ（`http://localhost:8080/users/new`）にアクセスすると下のような画面が出てきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/44233db4-8e2e-8972-8364-e7d9c8a93252.png)

POSTリクエストするあたりを説明します！

5行目：`<form @submit.prevent="post">`
formをレンダリングしてますが、`@submit.prevent`でsubmit時に22行目で定義している`post()`メソッドを呼び出してます。

22行目〜31行目：POSTリクエスト
大まかに形としては

```
this.$axios.post(url, data).then((res) => {成功した後の動作})
```

という感じです。
`url`には先ほど`proxy`で定義した`/api/`を用いて`/api/users`を指定します。これで`http://back:3000/users`にリクエストすることになります。
`data`にはリクエストデータを記載します。今回はform内で入力している`v-model="name"`の値をリクエストしたいので、`{ name: this.name }`としてます。
成功した後の動作としては登録したUserのHelloページへ遷移するとしてました。ページ遷移はメソッド内の場合は`this.$router.push(パス)`でできるので、レスポンスデータから登録された`id`を取得して、

```
this.$router.push(`${res.data.id}`)
```
とすることでPOSTリクエストに成功したときに作成したユーザーのHelloページへ自動遷移されます。

ここまで作成したら、Helloページ側のGETリクエスト先も`/api/`を使った記述に変更しておきます。これしないと`this.$router.push`時にリクエスト失敗しちゃいました。

```diff:front/pages/users/_id.vue
-    return $axios.$get(`http://back:3000/users/${params.id}`)
+    return $axios.$get(`/api/users/${params.id}`)
```

ここまでで完成です。実際に`http://localhost:8080/new`にアクセスしてみて`Name`を入力し`submit`してみましょう。

```
$ docker-compose up
```

入力したNameの「Hello, xxxxx」の画面に遷移したらNuxt.js + Rails(API)のHello worldは成功です！

# Afterword
Nuxt.js初体験、Vue.jsとかも触ったことがなかったので結構苦戦をしいられました...
特にaxiosのPOSTリクエストには...かなりの時間を...うぅ...
同じくHello worldに苦しむ方の助けになれば幸いです！
引き続き勉強していかねば〜。

# Reference
- [Nuxt.js - ユニバーサル Vue.js アプリケーション](https://ja.nuxtjs.org/)
- [docker で nuxt.js を開発環境を建てるだけ - Qiita](https://qiita.com/kitsuki00/items/ed51dbb254bcc6c94fbd)
- [Rails on Docker（alpine）でAPIコンテナをつくってみた - Qiita](https://qiita.com/at-946/items/70a5bd3ece095ead584a)
- [IT研修でVuePress+Express+Nuxt on Dockerでシステムを作成した話 - エンジニアの卵の成長日記](http://kurowasi2525.hatenablog.com/entry/2018/04/29/201924)
- [Axios モジュールの使いかた | Nuxt.js における REST API の活用](https://nuxt-api-state.lec.cafe/9.2.axios%20%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E4%BD%BF%E3%81%84%E3%81%8B%E3%81%9F/#axios-%E3%81%AE%E4%BD%BF%E3%81%84%E3%81%8B%E3%81%9F)
- [Nuxt.jsのメソッド内で外部APIを叩くとcorsエラーが起きる - Qiita](https://qiita.com/naokada/items/394c1d85ad2975ba62f0)
- [nuxt.js で axios から外部APIを叩くとCORSエラーを解決 - Qiita](https://qiita.com/penton310/items/4312ddbe015476fd67ff)
- [Hot reloading doesn't work in docker container on Windows · Issue #2481 · nuxt/nuxt.js](https://github.com/nuxt/nuxt.js/issues/2481)
- [プログラムによるナビゲーション | Vue Router](https://router.vuejs.org/ja/guide/essentials/navigation.html)