【初心者・クラシック中級者向け】RESTful デファクトになりつつあるフロント／バックエンドの完全分離サンプル(Nuxt.js + Rails API on Docker)

## はじめに

タイトルはあくまで個人的見解ですが、近年のプロダクト開発ではマイクロサービス／APIファースト、BFF(Backend For Frontends)といったアーキテクチャがよく取り込まれていると思われます。
[noteのフロントエンドをNuxt.jsへ刷新します｜こんぴゅ｜note](https://note.mu/konpyu/n/n9b7bf4343514)
[SPA開発未経験者によるNuxt.jsを使った自社サービス開発の裏側 - ログミーTech](https://logmi.jp/tech/articles/321241)
[レガシーシステムの大規模リプレイスで分かった「Vue.jsでSPAならNuxt.jsが有力」 - エンジニアHub｜若手Webエンジニアのキャリアを考える！](https://employment.en-japan.com/engineerhub/entry/2018/09/25/110000)
[スモールスタートではじめるSSR - dely engineering blog](https://tech.dely.jp/entry/min_ssr)
[BFFに取り組む開発者たちが語る「UIT#3 The “Backends for Frontends” sharing」 - LINE ENGINEERING](https://engineering.linecorp.com/ja/blog/bff-talk-uit-3-backends-for-front-ends-sharing/)

筆者(3年目のソフトウェアエンジニア[@_soyamaguchi_](https://twitter.com/_soyamaguchi_))はこれまで大規模でモノリシックなプロダクトの改修やスタンドアロンApp、ツール等の開発しか経験がなく、新規開発のAPIファーストな開発で当初慌てたため、本記事ではBFFとまではいかないにしてもRESTful APIでフロント／バックエンドを完全分離した簡易的なCRUD機能を持つWebAppサンプルをNuxt(SSR)+RailsAPIで作っていきます！

初学者の方にもご理解頂けるようにチュートリアル形式で環境構築から行っていきたいと思います。(本記事は[How to separate frontend + backend with Rails API, Nuxt.js and Devise-JWT](https://medium.com/@fishpercolator/how-to-separate-frontend-backend-with-rails-api-nuxt-js-and-devise-jwt-cf7dd9da9d16)、[ReactJS + Ruby on Rails API + Heroku App](https://medium.com/@bruno_boehm/reactjs-ruby-on-rails-api-heroku-app-2645c93f0814)を参考に執筆しております)

ほな、大きく分けて以下の構成で書いていきます:open_hands:

- **1 / 5　[前提及び準備(インストールなど)](#1--5-前提及び準備)**
- **2 / 5　[環境構築(Rails API, NuxtApp)](#2--5-環境構築)**
- **3 / 5　[バックエンドAPI作成(Rails API)](#3--5-バックエンドapi作成)**
- **4 / 5　[フロントエンド作成(Nuxt)](#4--5-フロントエンド作成)**
- **5 / 5　[CRUD](#5--5-crud)**

## 環境及び技術スタック

今回はMacでDockerを用いて環境構築をしていきます。(他のPC(OS)でもDockerが使用できれば問題ないかと思われます)

```text
 - Ruby 2.6.4
 - Rails 6.0.0
 - Node 12.10.0
 - yarn 1.17.3
 - PostgreSQL 11.5
```

## DEMO

以下が成果物です。
本当にシンプルな機能(リスト読み込み、追加、編集、削除)のみを持つサンプルアプリとなっています。

![nuxt-rails-sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491935/8a2b8c8e-a8cc-27a9-e8d0-1126f68c9f93.gif)

## 本記事で作成したコード

本記事で作成した最終的なコードはGitHubにアップしてあります。
こちらも参考にしてみてください。
[https://github.com/soyamaguchi/nuxt-rails-sample](https://github.com/soyamaguchi/nuxt-rails-sample)

それでは以下より本編となっております！

## 1 / 5 前提及び準備

まず、Ruby, Rails, Node, yarn, PostgreSQL, Dockerの環境を用意していきます。

### Homebrewのインストール

全てパッケージマネージャーのHomebrew, Homebrew Caskでインストールしていきますので、インストールされていなければ以下のページに記載されているインストールコマンドをコピーして実行してください。
Homebrew：[The missing package manager for macOS (or Linux) — Homebrew](https://brew.sh)
Homebrew Cask：[みんなhomebrew-caskって知ってるか？ - Qiita](https://qiita.com/ryurock/items/1432578d364985f6cb06)

```sh
$ brew -v
Homebrew 2.1.11
Homebrew/homebrew-core
Homebrew/homebrew-cask
```

### 各種環境のインストール

Homebrewのインストールが完了したら、上記の環境を用意するために以下のサイトを参考に各種インストールしてください。
Ruby：[rbenv を利用した Ruby 環境の構築 ｜ DevelopersIO](https://dev.classmethod.jp/server-side/language/build-ruby-environment-by-rbenv/)
Node, yarn：[install nodebrew, node and yarn - Qiita](https://qiita.com/morikiyo/items/cd219a0b7aaed5b23d51)
PostgreSQL：[[DB]MacにPostgreSQLをインストールする方法 - Qiita](https://qiita.com/shi-ma-da/items/f3cab71bd22cee2b9f98)
Docker：[Homebrew で macOS に Docker をインストールして Hello world - Qiita](https://qiita.com/niwasawa/items/c8cfe350caa9f4730400#_reference-fe861645e63e736ad6f7)

```sh
$ ruby -v
ruby 2.6.4p104 (2019-08-28 revision 67798) [x86_64-darwin18]

$ node -v
v12.10.0

$ yarn -v
1.17.3

$ psql -V
psql (PostgreSQL) 11.5

$ docker version
Client: Docker Engine - Community
 Version:           19.03.2
 API version:       1.40
 Go version:        go1.12.8
 Git commit:        6a30dfc
 Built:             Thu Aug 29 05:26:49 2019
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.2
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.8
  Git commit:       6a30dfc
  Built:            Thu Aug 29 05:32:21 2019
  OS/Arch:          linux/amd64
  Experimental:     false
```

インストールできましたでしょうか？
環境ができましたら以下のコマンドでRailsをインストールしておいてください。

```sh
$ gem i rails -v 6.0.0

$ rails -v
Rails 6.0.0
```

とりあえず、ここまでで実行環境の準備は終わりです！
次に環境構築を行っていきます。

## 2 / 5 環境構築

### プロジェクト生成

先にRails APIとNuxtAppを作成していきます。

```sh
$ mkdir nuxt-rails-sample
$ cd nuxt-rails-sample
$ rails new backend --api -d postgresql
$ yarn create nuxt-app frontend
? Project name frontend
? Project description nuxt-rails-sample frontend
? Author name Soya Yamaguchi
? Choose the package manager Yarn
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)Axios, Progressive Web App (PWA) Support
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)ESLint, Prettier, Lint staged files
? Choose test framework Jest
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection)jsconfig.json (Recommended for VS Code)
```

 - Railsのプロジェクト生成時に`--api`オプション付けてあげることでAPIモードになります。
 - Nuxtのプロジェクト生成時にいくつか質問されますが、今回は上記のように設定してください。([インストール - Nuxt.js](https://ja.nuxtjs.org/guide/installation/))

### Dockerでのコンテナ作成

次に`Dockerfile`を作成してフロント／バックエンド環境を構築し、`docker-compose.yml`を作成していきます。

```text:DirStructure
/
├──frontend
│  ├──Dockerfile
├──backend
│  ├──Dockerfile
├──docker-compose.yml

```

```dockerfile:frontend/Dockerfile
FROM node:12.10

ENV APP_DIR /app/frontend
ENV PATH /app/frontend/node_modules/.bin:$PATH
ENV TZ Asia/Tokyo
ENV HOST 0.0.0.0

RUN mkdir -p /app/frontend
WORKDIR $APP_DIR
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
COPY package.json $APP_DIR/package.json
COPY yarn.lock $APP_DIR/yarn.lock
RUN yarn install
COPY . $APP_DIR
```

```dockerfile:backend/Dockerfile
FROM ruby:2.6.4

RUN apt-get update -qq && apt-get install -y build-essential libpq-dev postgresql-client nodejs

ENV APP_DIR /app/backend

RUN mkdir -p /app/backend
WORKDIR $APP_DIR
COPY Gemfile $APP_DIR/Gemfile
COPY Gemfile.lock $APP_DIR/Gemfile.lock
RUN bundle install
COPY . $APP_DIR
```

```yml:docker-compose.yml
version: '3'
services:
  db:
    image: postgres:11.5
    volumes:
      - ./backend/db/pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"  
    networks:
      app_net:
        ipv4_address: 192.168.32.2

  backend:
    build: ./backend
    command: /bin/bash -c "rm -rf tmp/pids/server.pid; bundle exec rails s -p 8080 -b 0.0.0.0"
    volumes:
      - ./backend:/app/backend
    ports:
      - "8080:8080"
    depends_on:
      - db
    networks:
      app_net:
        ipv4_address: 192.168.32.3
    tty: true
    stdin_open: true

  frontend:
    build: ./frontend
    command: yarn dev
    volumes:
      - ./frontend:/app/frontend
    ports:
      - "3000:3000"
    depends_on:
      - backend
    networks:
      app_net:
        ipv4_address: 192.168.32.4
    tty: true

networks:
  app_net:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.32.0/20
```

ここでコンテナのサブネットとIPを固定にしているのはなぜかコンテナ間の名前解決ができなかったためです。。。
本来はサービス名で名前解決できるみたいですが、筆者の環境ではfrontendからbackendに通信できず最終的にこのような形をとりました。(ハードコーディングしていますが、環境変数や.envファイルで読み込むべきでしょう)

次に`backend/.gitignore`を編集します。
`docker-compose.yml`のvolumesでマウントしている`backend/db/pgdata`と管理外にしたい`backend/vendor/bundle`を追記し、Gitの管理下から外します。

```diff:backend/.gitignore
+/vendor/bundle
+/db/pgdata
```

次に`database.yml`を編集します。

```diff:backend/config/database.yml
default: &default
   adapter: postgresql
   encoding: unicode
   pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+  host: db
+  username: postgres
+  password:
```

ここまでくればあと少しで環境構築が終わります！

作成したDockerファイルをビルドしておきましょう。

```sh
$ docker-compose build
```

ビルドには少々時間が掛かるかもしれません:thinking:
ビルドするとdockerイメージが生成されます。

```sh
$ docker images
REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
nuxt-rails-sample_backend    latest              1d60ede04488        16 minutes ago      1.16GB
nuxt-rails-sample_frontend   latest              493c2ee51fdc        43 hours ago        1.31GB
ruby                         2.6.4               121862ceb25f        2 weeks ago         840MB
postgres                     11.5                e2d75d1c1264        2 weeks ago         313MB
node                         12.10               d8c33ae35f44        2 weeks ago         907MB
```

ビルドが完了しましたらbackendコンテナでDBを作成します。

```sh
$ docker-compose run --rm backend rails db:create
```

最後は以下のコマンドを実行します。

```sh
$ docker-compose up
```

無事にDB, backend, frontendのコンテナが生成され起動していますでしょうか？
以下のようにSTATUSがUpになっていたら起動している状態です。

```sh
$ docker-compose ps -a
CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS                    NAMES
15b56bba7d9d        nuxt-rails-sample_frontend   "docker-entrypoint.s…"   2 minutes ago       Up 2 minutes        0.0.0.0:3000->3000/tcp   nuxt-rails-sample_frontend_1
b7d00b7a30f0        nuxt-rails-sample_backend    "/bin/bash -c 'rm -r…"   2 minutes ago       Up 2 minutes        0.0.0.0:8080->8080/tcp   nuxt-rails-sample_backend_1
78daac31a23c        postgres:11.5                "docker-entrypoint.s…"   3 minutes ago       Up 3 minutes        0.0.0.0:5432->5432/tcp   nuxt-rails-sample_db_1
```

コンテナを止めるときは`docker-compose stop`で止めて、開始させるときは`docker-compose start`で開始させます。
またコンテナとネットワークを削除したいときは`docker-compose down`で削除します。

dockerコマンドの詳細は調べて頂くとすぐに出てきますのでご自身で調べて頂きますようよろしくお願いします:bow:

ここまで順調に来ておればブラウザでNuxtとRailsのデフォルトページが表示されるはずです！

<img width="1899" alt="スクリーンショット 2019-09-26 23.19.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491935/e639e553-4bcf-d475-aeb9-d5772ab7dc52.png">

お疲れ様です！これで環境構築が完了しました:raised_hands:
次からはフロント／バックエンドを作成していきます！

## 3 / 5 バックエンドAPI作成

### リソースの生成

APIを作成していくのにまずリソースを生成します。
最初に以下のコマンドでbackendコンテナに接続します。(切断するときは`exit`または<kbd><kbd>Ctrl</kbd>+<kbd>D</kbd></kbd>で切断してくださいね)

```sh
$ docker-compose exec backend bash
```

次に以下のコマンドでリソースを生成します。

```sh
root@b7d00b7a30f0:/app/backend# rails g scaffold List title:string excerpt:text
Running via Spring preloader in process 55
      invoke  active_record
      create    db/migrate/20190928095923_create_lists.rb
      create    app/models/list.rb
      invoke    test_unit
      create      test/models/list_test.rb
      create      test/fixtures/lists.yml
      invoke  resource_route
       route    resources :lists
      invoke  scaffold_controller
      create    app/controllers/lists_controller.rb
      invoke    test_unit
      create      test/controllers/lists_controller_test.rb

root@b7d00b7a30f0:/app/backend# rails db:migrate
== 20190928095923 CreateLists: migrating ======================================
-- create_table(:lists)
   -> 0.0986s
== 20190928095923 CreateLists: migrated (0.1014s) =============================
```

上記が完了したらいくつかのデータをDBに追加していきます。

```ruby:backend/db/seeds.rb
3.times {|n| List.create(title: "Test-title-#{n}", excerpt: "Test-excerpt-#{n}")}
```
```sh
root@b7d00b7a30f0:/app/backend# rails db:seed
```

### APIコントローラの編集

DBにデータを追加し終えたら`routes.rb`を編集します。

```ruby:backend/config/routes.rb
Rails.application.routes.draw do
  namespace :api, format: 'json' do
    namespace :v1 do
      resources :lists
    end
  end
end
```

上記のnamespaceを使用してcontrollerにアクセスするために`lists_controller.rb`も編集していきます。

まず`backend/controllers`直下に`api/v1`ディレクトリを作成し、`lists_controller.rb`を移動させます。
移動後は以下のような構成になります。

```text
backend
├──app
   ├──controllers
      ├──api
      │   ├──v1
      │      ├──lists_controller.rb
      ├──concerns
      ├──application_controller.rb
```

移動後は`routes.rb`で設定したnamespace`Api::V1`でラップします。

```ruby:backend/app/controllers/api/v1/lists_controller.rb
module Api::V1
  class ListsController < ApplicationController
    before_action :set_list, only: [:show, :update, :destroy]

    # GET /lists
    def index
      @lists = List.all

      render json: @lists
    end

    # GET /lists/1
    def show
      render json: @list
    end

    # POST /lists
    def create
      @list = List.new(list_params)

      if @list.save
        render json: @list, status: :created, location: @list
      else
        render json: @list.errors, status: :unprocessable_entity
      end
    end

    # PATCH/PUT /lists/1
    def update
      if @list.update(list_params)
        render json: @list
      else
        render json: @list.errors, status: :unprocessable_entity
      end
    end

    # DELETE /lists/1
    def destroy
      @list.destroy
    end

    private
      # Use callbacks to share common setup or constraints between actions.
      def set_list
        @list = List.find(params[:id])
      end

      # Only allow a trusted parameter "white list" through.
      def list_params
        params.require(:list).permit(:title, :excerpt)
      end
  end
end
```

後々のことを考えて少し修正しておきます。
ここでは単純に`/api/v1/lists`にgetリクエストが来たときにListを昇順で返す、`/api/v1/lists`にpostリクエストが来たときにRails側では画面遷移について操作しないように変更しています。

```diff:backend/app/controllers/api/v1/lists_controller.rb
     # GET /lists
     def index
-      @lists = List.all
+      @lists = List.all.order(:id)
 
       render json: @lists
     end

     # POST /lists
     def create
       @list = List.new(list_params)
 
       if @list.save
-        render json: @list, status: :created, location: @list
+        render json: @list, status: :created
       else
         render json: @list.errors, status: :unprocessable_entity
       end
     end
```

これで[http://localhost:8080/api/v1/lists](http://localhost:8080/api/v1/lists)にアクセスしてAPIの動作確認ができるようになりました！

<img width="1614" alt="スクリーンショット 2019-09-28 22.02.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491935/6ad51382-8c1f-c791-ddd2-54193a976856.png">

とりあえずここまででAPIの作成は以上になります。

ここからはフロントエンドを作成してバックエンドに接続していきます！

## 4 / 5 フロントエンド作成

### Proxy設定

まずフロントエンドからのリクエストをAPIのエンドポイントにプロキシさせるために`@nuxtjs/axios`に`proxy`の設定を追加します。

以下のように設定することでフロントエンドから`/api/v1/*`を叩くことによって、先程作成したbackendコンテナのAPIエンドポイントにアクセス可能になります。

```diff:frontend/nuxt.config.js
   /*
    ** Axios module configuration
    ** See https://axios.nuxtjs.org/options
    */
-  axios: {},
+  axios: {
+    proxy: true
+  },
+  proxy: {
+    '/api/v1/': {
+      // backendのコンテナip
+      target: 'http://192.168.32.3:8080',
+      pathRewrite: {
+        '^/api/v1/': '/api/v1/'
+      },
+    }
+  },
```

### CORS設定

また他のドメインのCORSヘッダーを適切に設定しない限り、他のドメインのエンドポイントにアクセスすることは許可されません。([CORSまとめ - Qiita](https://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34))
なのでここでRails APIの設定を変更します。

デフォルトで用意されているGemfileの`gem 'rack-cors'`の行のコメントアウトを解除し、`backend/config/initializers/cors.rb`を編集します。
編集後は以下のようになります。

```diff:backend/Gemfile
-# gem 'rack-cors'
+gem 'rack-cors'
```

```diff:backend/config/initializers/cors.rb
-# Rails.application.config.middleware.insert_before 0, Rack::Cors do
-#   allow do
-#     origins 'example.com'
-#
-#     resource '*',
-#       headers: :any,
-#       methods: [:get, :post, :put, :patch, :delete, :options, :head]
-#   end
-# end
+Rails.application.config.middleware.insert_before 0, Rack::Cors do
+  allow do
+    # frontendのコンテナip
+    origins '192.168.32.4:3000'
+
+    resource '*',
+      headers: :any,
+      methods: [:get, :post, :put, :patch, :delete, :options, :head]
+  end
+end
```

上記のように編集が完了したらコメントアウトしたgemをインストールします。

```sh
root@b7d00b7a30f0:/app/backend# bundle
```

一応ここらで一回コンテナを削除して作り直しておきましょう。

```sh
$ docker-compose down

$ docker-compose up
```

これでフロントエンドからバックエンドAPIへのリクエスト処理の準備が完了しました！

### フロントエンドの作成

次にNuxtAppの`layouts/default.vue`と`pages/index.vue`を以下のように修正してください。
また`pages/inspire.vue`は不要なのでファイルごと削除しておきます。

```vue:frontend/layouts/default.vue
<template>
  <div id="app">
    <v-app>
      <v-toolbar>
        <n-link id="top" to="/">Nuxt-Rails-Sample</n-link>
      </v-toolbar>
      <v-content>
        <v-container fluid>
          <nuxt />
        </v-container>
      </v-content>
    </v-app>
  </div>
</template>

<style lang="scss">
body {
  .v-toolbar {
    &__content {
      padding: 0 2rem;
      a {
        text-decoration: none;
        color: #616161;
        font-weight: bold;
        font-size: 1.5rem;
      }
    }
  }
  .container {
    display: flex;
    min-height: 100vh;
    padding: 2rem;
  }
}
</style>
```

```vue:frontend/pages/index.vue
<template>
  <div class="home">
    <template v-if="editTargetList">
      <EditListForm :list="editTargetList" @set="editingList" />
    </template>
    <template v-else>
      <NewListForm />
      <ListsContainer @set="editingList" />
    </template>
  </div>
</template>

<script>
import ListsContainer from '~/components/ListsContainer.vue'
import NewListForm from '~/components/NewListForm.vue'
import EditListForm from '~/components/EditListForm.vue'

export default {
  components: {
    ListsContainer,
    NewListForm,
    EditListForm
  },
  data() {
    return {
      lists: [],
      editTargetList: ''
    }
  },
  async asyncData({ $axios }) {
    const data = await $axios.$get('/api/v1/lists')
    return { lists: data }
  },
  methods: {
    editingList(list = '') {
      this.editTargetList = list
    }
  }
}
</script>

<style lang="scss" scoped>
.home {
  width: 100vw;
}

.flex {
  margin-bottom: 2rem;
}
</style>
```

後程、処理内容は説明しますが、一旦ここで`index.vue`でimportしているcomponentも以下のように作成しておきましょう。

```vue:frontend/components/ListsContainer.vue
<template>
  <v-card>
    <v-list-item v-for="list in this.$parent.lists" :key="list.id" two-line>
      <v-list-item-content>
        <h2>{{ list.title }}</h2>
      </v-list-item-content>
      <v-btn class="mx-2" fab dark color="pink" @click="removeList(list.id)">
        <v-icon>mdi-minus</v-icon>
      </v-btn>
      <v-btn class="mx-2" fab dark color="cyan" @click="$emit('set', list)">
        <v-icon>mdi-pencil</v-icon>
      </v-btn>
    </v-list-item>
  </v-card>
</template>

<script>
export default {
  methods: {
    removeList(id) {
      this.$axios
        .delete(`/api/v1/lists/${id}`)
        .then((res) => {
          const lists = this.$parent.lists.filter((l) => l.id !== id)
          this.$parent.lists = lists
        })
        .catch((err) => {
          console.log(err)
        })
    }
  }
}
</script>
```

```vue:frontend/components/NewListForm.vue
<template>
  <v-flex>
    <v-card>
      <v-card-text>
        <v-form>
          <v-text-field v-model="title" label="title" />
          <v-text-field v-model="excerpt" label="excerpt" type="text" />
        </v-form>
        <v-card-actions>
          <v-btn class="mx-2" fab dark color="indigo" @click="addList">
            <v-icon>mdi-plus</v-icon>
          </v-btn>
        </v-card-actions>
      </v-card-text>
    </v-card>
  </v-flex>
</template>

<script>
export default {
  data() {
    return {
      title: '',
      excerpt: ''
    }
  },
  methods: {
    addList() {
      this.$axios
        .$post('/api/v1/lists', { title: this.title, excerpt: this.excerpt })
        .then((res) => {
          this.$parent.lists.push(res)
        })
        .catch((err) => {
          console.log(err)
        })
    }
  }
}
</script>
```

```vue:frontend/components/EditListForm.vue
<template>
  <v-flex>
    <v-card>
      <v-card-text>
        <v-form>
          <v-text-field v-model="title" label="title" />
          <v-text-field v-model="excerpt" label="excerpt" />
        </v-form>
        <v-card-actions>
          <v-btn class="mx-2" fab dark color="teal" @click="editList">
            <v-icon dark>mdi-pencil</v-icon>
          </v-btn>
        </v-card-actions>
      </v-card-text>
    </v-card>
  </v-flex>
</template>

<script>
export default {
  props: {
    list: {
      type: Object,
      default: null
    }
  },
  data() {
    return {
      id: this.list.id,
      title: this.list.title,
      excerpt: this.list.excerpt
    }
  },
  methods: {
    editList() {
      this.$axios
        .$put(`/api/v1/lists/${this.id}`, {
          title: this.title,
          excerpt: this.excerpt
        })
        .then((res) => {
          const lists = this.$parent.lists.map((l) => {
            return l.id === this.id ? res : l
          })
          this.$parent.lists = lists
          this.$emit('set')
        })
        .catch((err) => {
          console.log(err)
        })
    }
  }
}
</script>
```

また現状はVuetifyの設定でダークモードになっているため、以下のようにデフォルト(白)に戻しておきます。

```diff:frontend/nuxt.config.js
   vuetify: {
     customVariables: ['~/assets/variables.scss'],
     theme: {
-      dark: true,
+      // dark: true,
       themes: {
         dark: {
```

とりあえずこれでフロントエンドは作成できました！
以下のように表示されていますでしょうか？
<img width="1614" alt="スクリーンショット 2019-09-29 17.05.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491935/f9efcd08-35bc-a569-fb37-656318683d89.png">

フロントエンドはバックエンドからのレスポンスデータ(JSON)を表示しています！
titleとexcerptを入力後、＋ボタンを押してDBにリストを追加して、フロントエンドが実際にバックエンドからデータを取得していることを確認してみてください。

次は今回作成したフロントエンドからのリクエスト処理(CRUD機能)について簡単に説明していきます！

## 5 / 5 CRUD

先に前提とレイアウトについて少しだけ説明しておきます。

### 前提

前提として、まず共通のレイアウトとなる`default.vue`の`<nuxt />`コンポーネントに`pages`配下のページコンポーネントがレンダリングされます。
ページコンポーネントのディレクトリ構成に従って、自動的にVue Router([紹介 | Vue Router](https://router.vuejs.org/ja/))の設定を行ってくれているのでNuxtを使用する場合、特にルーティングに気を配ることはないかと思われます。

```vue:frontend/layouts/default.vue
<template>
  <div id="app">
    <v-app>
      <v-toolbar>
        <n-link id="top" to="/">Nuxt-Rails-Sample</n-link>
      </v-toolbar>
      <v-content>
        <v-container fluid>
          <nuxt />
        </v-container>
      </v-content>
    </v-app>
  </div>
</template>

<style lang="scss">
body {
  .v-toolbar {
    &__content {
      padding: 0 2rem;
      a {
        text-decoration: none;
        color: #616161;
        font-weight: bold;
        font-size: 1.5rem;
      }
    }
  }
  .container {
    display: flex;
    min-height: 100vh;
    padding: 2rem;
  }
}
</style>
```

今回の場合、[http://localhost:3000](http://localhost:3000)にアクセスした時、`default.vue`の`<nuxt />`に`pages/index.vue`のページコンポーネントがレンダリングされます。

### レイアウト

全体的にレイアウトについてはCSSをほとんど書かず、Vuetify(マテリアルデザインのコンポーネントフレームワーク)でUIを作成しています。
Vuetifyを使用したことのない方(筆者は初めて使いました)からすると身に覚えのないタグが多数存在しているかと思われますが、以下の公式より検索できます。
[API explorer — Vuetify.js](https://vuetifyjs.com/ja/components/api-explorer)

### CRUD - READ

まず[http://localhost:3000](http://localhost:3000)にアクセス時、`pages/index.vue`内の`asyncData`が実行されます。
`asyncData`はページコンポーネントが読み込まれる前やSSR(Server Side Rendering)、ページ遷移前に呼び出され、コンポーネントのdataに値をセットすることができます。

```vue:frontend/pages/index.vue
<template>
  <div class="home">
    <template v-if="editTargetList">
      <EditListForm :list="editTargetList" @set="editingList" />
    </template>
    <template v-else>
      <NewListForm />
      <ListsContainer @set="editingList" />
    </template>
  </div>
</template>

<script>
import ListsContainer from '~/components/ListsContainer.vue'
import NewListForm from '~/components/NewListForm.vue'
import EditListForm from '~/components/EditListForm.vue'

export default {
  components: {
    ListsContainer,
    NewListForm,
    EditListForm
  },
  data() {
    return {
      lists: [],
      editTargetList: ''
    }
  },
  async asyncData({ $axios }) {
    const data = await $axios.$get('/api/v1/lists')
    return { lists: data }
  },
  methods: {
    editingList(list = '') {
      this.editTargetList = list
    }
  }
}
</script>

<style lang="scss" scoped>
.home {
  width: 100vw;
}

.flex {
  margin-bottom: 2rem;
}
</style>
```

上記の`asyncData`の処理内容はaxiosを使ってAPIを叩き(Rails APIの`/api/v1/lists`にGETリクエストを送り)、Listを昇順にしたJSONをdataの**lists**にマージします。
ここでリクエスト送信先の`/api/v1/lists`は`nuxt.config.js`で以下のように設定したAPIエンドポイントになります。
コンテナ間の通信なのでipで指定していますが、実際は[http://localhost:8080/api/v1/lists](http://localhost:8080/api/v1/lists)にリクエストを送信していると思っていただければ問題ないかと思われます。

```js:frontend/nuxt.config.js
axios: {
    proxy: true
  },
  proxy: {
    '/api/v1/': {
      // backendのコンテナip
      target: 'http://192.168.32.3:8080',
      pathRewrite: {
        '^/api/v1/': '/api/v1/'
      }
    }
  }
```

初期表示時はdataの**editTargetList**が`""`のため`<template v-if=false>`になり、importされている`NewListForm.vue`と`ListsContainer.vue`が表示されます。

`NewListForm.vue`は新たにListを追加するためのコンポーネントです。
`ListsContainer.vue`は親コンポーネントである`index.vue`の**lists**を表示し、削除、更新を行うコンポーネントとなっています。

実際に`this.$parent.lists`で親の**lists**を参照し、**lists**分のtitleと削除ボタン、編集ボタンを表示しています。

```vue:frontend/components/ListsContainer.vue
<template>
  <v-card>
    <v-list-item v-for="list in this.$parent.lists" :key="list.id" two-line>
      <v-list-item-content>
        <h2>{{ list.title }}</h2>
      </v-list-item-content>
      <v-btn class="mx-2" fab dark color="pink" @click="removeList(list.id)">
        <v-icon>mdi-minus</v-icon>
      </v-btn>
      <v-btn class="mx-2" fab dark color="cyan" @click="$emit('set', list)">
        <v-icon>mdi-pencil</v-icon>
      </v-btn>
    </v-list-item>
  </v-card>
</template>

<script>
export default {
  methods: {
    removeList(id) {
      this.$axios
        .delete(`/api/v1/lists/${id}`)
        .then((res) => {
          const lists = this.$parent.lists.filter((l) => l.id !== id)
          this.$parent.lists = lists
        })
        .catch((err) => {
          console.log(err)
        })
    }
  }
}
</script>
```

ここまでで以下の画面ができあがることになります。
<img width="1614" alt="スクリーンショット 2019-09-29 17.05.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491935/f9efcd08-35bc-a569-fb37-656318683d89.png">

以上がRead機能の説明になります。
かなり簡単に説明させて頂きましたが、残りの3機能もこのような感じで説明していきます。

### CRUD - CREATE

[CRUD - READ](#crud---read)で`NewListForm.vueは新たにListを追加するためのコンポーネントです。`と記載した内容について説明していきます。

`NewListForm.vue`はtitleとexcerpt項目が入力できるテキストボックスと`@click`イベント付きの追加ボタンが存在しているくらいです。

`v-model`は双方向データバインディングと呼ばれており、ユーザの入力に基づいて自動的にdataのオブジェクトが更新されます。なのでテキストボックスの入力値とdata.title, data.excerptは常に同じ状態になります。

上記になにか入力して、＋ボタンを押下するとボタンの`@click`イベントが発火し、`addList()`メソッドが実行されます。
`addList()`メソッドはaxiosでRails APIの`/api/v1/lists`にPOSTリクエストとパラメータ(data)を送り、API側は送られてきたパラメータに基づいてListを作成し、保存します。
リクエスト成功時に`this.$parent.lists.push(res)`が実行され`index.vue`の**lists**の末尾にPOST成功したListが追加されます。

```vue:frontend/components/NewListForm.vue
<template>
  <v-flex>
    <v-card>
      <v-card-text>
        <v-form>
          <v-text-field v-model="title" label="title" />
          <v-text-field v-model="excerpt" label="excerpt" type="text" />
        </v-form>
        <v-card-actions>
          <v-btn class="mx-2" fab dark color="indigo" @click="addList">
            <v-icon>mdi-plus</v-icon>
          </v-btn>
        </v-card-actions>
      </v-card-text>
    </v-card>
  </v-flex>
</template>

<script>
export default {
  data() {
    return {
      title: '',
      excerpt: ''
    }
  },
  methods: {
    addList() {
      this.$axios
        .$post('/api/v1/lists', { title: this.title, excerpt: this.excerpt })
        .then((res) => {
          this.$parent.lists.push(res)
        })
        .catch((err) => {
          console.log(err)
        })
    }
  }
}
</script>
```

`ListsContainer.vue`は`index.vue`の**lists**分表示しているため、上記の`addList()`で追加されたListも以下のように表示されます。

![ListsContainer.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491935/d3d726dd-7d86-fef5-4886-9a8241429f80.gif)

### CRUD - UPDATE

#### 編集画面の表示

まず`ListsContainer.vue`の編集ボタンを押下すると`@click`イベントが発火します。

```vue:frontend/components/ListContainer.vue
<v-btn class="mx-2" fab dark color="cyan" @click="$emit('set', list)">
  <v-icon>mdi-pencil</v-icon>
</v-btn>
```

`@click`イベントは、`$emit('set', list)`を実行します。
処理内容は親コンポーネントである`index.vue`で登録された`@set`イベントを発火します。
`@set`イベントには以下のように`editingList`メソッドが登録されており、こちらが実行されます。

```vue:frontend/pages/index.vue
<template v-else>
  <NewListForm />
  <ListsContainer @set="editingList" />
</template>
```

引数として`$emit`の第2引数に設定している選択対象の`list`が`editingList`メソッドに渡されます。
その渡された`list`をdataの**editTargetList**に設定しています。

```js:frontend/pages/index.vue
data() {
  return {
    lists: [],
    editTargetList: ''
  }
},
async asyncData({ $axios }) {
  const data = await $axios.$get('/api/v1/lists')
  return { lists: data }
},
methods: {
  editingList(list = '') {
    this.editTargetList = list
  }
}
```

簡単に説明すると、編集ボタン押下後、`index.vue`の`editingList`メソッドが実行され、引数で渡された選択対象のListをdataの**editTargetList**に設定する流れとなっています。

ここでdataの**editTargetList**に値が設定されると`<template v-if="editTargetList">`は`true`になり`EditListForm.vue`が表示対象になり編集画面が表示されます！

```vue:frontend/pages/index.vue
<template v-if="editTargetList">
  <EditListForm :list="editTargetList" @set="editingList" />
</template>
```

#### 編集処理

`EditListForm.vue`でも`@set`イベントが使えるように`@set`イベントに`editingList`メソッドを登録しています。
またdataの**editTargetList**も使えるようにするために`:list="editTargetList"`としています。

```vue:frontend/pages/index.vue
<template v-if="editTargetList">
  <EditListForm :list="editTargetList" @set="editingList" />
</template>
```

上記を`EditListForm.vue`で受け取るためにプロパティ(`props`)を使います。
`EditListForm.vue`は編集画面であり、初期値として受け取ったプロパティを変化させたいため、`props`で受け取った後、ローカルデータとして使用するためにプロパティの値をdataの初期値として定義します。

```vue:frontend/components/EditListForm.vue
<template>
  <v-flex>
    <v-card>
      <v-card-text>
        <v-form>
          <v-text-field v-model="title" label="title" />
          <v-text-field v-model="excerpt" label="excerpt" />
        </v-form>
        <v-card-actions>
          <v-btn class="mx-2" fab dark color="teal" @click="editList">
            <v-icon dark>mdi-pencil</v-icon>
          </v-btn>
        </v-card-actions>
      </v-card-text>
    </v-card>
  </v-flex>
</template>

<script>
export default {
  props: {
    list: {
      type: Object,
      default: null
    }
  },
  data() {
    return {
      id: this.list.id,
      title: this.list.title,
      excerpt: this.list.excerpt
    }
  },
  methods: {
    editList() {
      this.$axios
        .$put(`/api/v1/lists/${this.id}`, {
          title: this.title,
          excerpt: this.excerpt
        })
        .then((res) => {
          const lists = this.$parent.lists.map((l) => {
            return l.id === this.id ? res : l
          })
          this.$parent.lists = lists
          this.$emit('set')
        })
        .catch((err) => {
          console.log(err)
        })
    }
  }
}
</script>
```

このようにすることでList追加時([CRUD - CREATE](#crud---create))と同様にtitleとexcerptのテキストボックスは`v-model`で双方向データバインディングされ、以下の修正ボタンを押下すると`editList`メソッドが実行され、PUTリクエストが送信されます。

```vue:frontend/components/EditListForm.vue
<v-btn class="mx-2" fab dark color="teal" @click="editList">
  <v-icon dark>mdi-pencil</v-icon>
</v-btn>
```

`editList`メソッドはRails APIの`/api/v1/lists/:id(編集対象ListId)`にPUTリクエストとパラメータ(data)を送り、API側は送られてきたパラメータで対象のListを更新します。
リクエスト成功時に`index.vue`の**lists**に修正した新たなListを含むlistsを設定して、最後に`@set`イベントを発火させます。

```js:frontend/components/EditListForm.vue
data() {
  return {
    id: this.list.id,
    title: this.list.title,
    excerpt: this.list.excerpt
  }
},
methods: {
  editList() {
    this.$axios
      .$put(`/api/v1/lists/${this.id}`, {
        title: this.title,
        excerpt: this.excerpt
      })
      .then((res) => {
        const lists = this.$parent.lists.map((l) => {
          return l.id === this.id ? res : l
        })
        this.$parent.lists = lists
        this.$emit('set')
      })
      .catch((err) => {
        console.log(err)
      })
  }
}
```

`@set`イベントに登録されている`editingList`メソッドが実行されますが、引数(List)が存在しないため、デフォルト引数で指定されている`""`がdataの**editTargetList**に設定されます。

```js:frontend/pages/index.vue
methods: {
  editingList(list = '') {
    this.editTargetList = list
  }
}
```

dataの**editTargetList**が`""`になると`<template v-if="editTargetList">`は`false`になるため、編集画面は表示されず初期画面が表示され、更新処理が完了となります。

```vue:frontend/pages/index.vue
<template v-else>
  <NewListForm />
  <ListsContainer @set="editingList" />
</template>
```

![CRUD-Update.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491935/20ca4b54-3ee7-b34c-0a01-484d1049a8f4.gif)

### CRUD - DELETE

DELETEも他の機能とほとんど要領です。

まず初期画面に表示されているListの削除ボタン押下で`@click`イベントが発火し、`removeList`メソッドが実行されます。

```vue:frontend/components/ListsContainer.vue
<v-btn class="mx-2" fab dark color="pink" @click="removeList(list.id)">
  <v-icon>mdi-minus</v-icon>
</v-btn>
```

`removeList`メソッドではRails APIの`/api/v1/lists/:id(削除対象ListId)`にDELETEリクエストを送り、API側で対象のListを削除します。
リクエスト成功時に`index.vue`の**lists**に削除したListを含まないlistsを設定します。

```js:frontend/components/ListsContainer.vue
<script>
export default {
  methods: {
    removeList(id) {
      this.$axios
        .delete(`/api/v1/lists/${id}`)
        .then((res) => {
          const lists = this.$parent.lists.filter((l) => l.id !== id)
          this.$parent.lists = lists
        })
        .catch((err) => {
          console.log(err)
        })
    }
  }
}
</script>
```

これで削除処理は完了となります。

![CRUD-DELETE.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491935/72f28881-bc34-2ceb-e304-9199758dfab4.gif)

簡易的なCRUDでしたが、説明は以上となります。

## 最後に

これで最初の[DEMO](#demo)で紹介した通りのWebAppができているかと思われます。

フロントエンドとバックエンドを完全に分離すると設計が綺麗になったように思います。
唯一の接点がレスポンスデータであるJSONになっていることで各分野のエンジニアは自分の担当分野に専念でき、リポジトリ管理も別々にすることも可能ですし、開発スピードの向上にも繋がるのではないでしょうか。もちろんフロントとバックでしっかりとネゴを取っておくことは重要ですが。

フロント／バックエンドを完全分離し責務をしっかりと分け、リソースごとにAPIエンドポイントを作って(マイクロサービス)、BFFにすれば割とモダンなアーキテクチャになるのではないでしょうか:thinking:
あとはプロダクトごとでフロント／バックエンドの設計が変わるくらいなのではと思っています。

今回はただフロント／バックエンドを完全分離しただけの記事になってしまいましたが、またこのような類の記事を執筆するときは、機能をいくつか追加し、BFFにして本番環境へのデプロイまで行うような流れを書けたらと思っております。

かなり長くなってしまいましたが、ご覧いただきありがとうございました:bow_tone1:

## 参考文献

本文中にも明記しておりますが、以下にもまとめておきます。

- [How to separate frontend + backend with Rails API, Nuxt.js and Devise-JWT](https://medium.com/@fishpercolator/how-to-separate-frontend-backend-with-rails-api-nuxt-js-and-devise-jwt-cf7dd9da9d16)
- [ReactJS + Ruby on Rails API + Heroku App](https://medium.com/@bruno_boehm/reactjs-ruby-on-rails-api-heroku-app-2645c93f0814)
- [The missing package manager for macOS (or Linux) — Homebrew](https://brew.sh)
- [みんなhomebrew-caskって知ってるか？ - Qiita](https://qiita.com/ryurock/items/1432578d364985f6cb06)
- [rbenv を利用した Ruby 環境の構築 ｜ DevelopersIO](https://dev.classmethod.jp/server-side/language/build-ruby-environment-by-rbenv/)
- [install nodebrew, node and yarn - Qiita](https://qiita.com/morikiyo/items/cd219a0b7aaed5b23d51)
- [[DB]MacにPostgreSQLをインストールする方法 - Qiita](https://qiita.com/shi-ma-da/items/f3cab71bd22cee2b9f98)
- [Homebrew で macOS に Docker をインストールして Hello world - Qiita](https://qiita.com/niwasawa/items/c8cfe350caa9f4730400#_reference-fe861645e63e736ad6f7)
- [インストール - Nuxt.js](https://ja.nuxtjs.org/guide/installation/)
- [CORSまとめ - Qiita](https://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34)
- [紹介 | Vue Router](https://router.vuejs.org/ja/)
- [API explorer — Vuetify.js](https://vuetifyjs.com/ja/components/api-explorer)