RailsとVue.js(SPA)を「いいとこ取り」。API連携で開発するハンズオン。（その1：Rails編）

2020.03.07追記：[続編を書きました！](https://qiita.com/shozzy/items/25d2f999530fa47627a9)

# はじめに

DBの操作と管理画面はRailsで作成しつつ、一般ユーザ向けの画面はSPAとかPWAにしたいというシーンは結構あるかと思います。既存アプリがRailsで動いているのを活かしながら、フロント側はSPA化するとか。基本はSPAなんだけど、管理画面はscaffoldでサクッと作って済ませたいとか。

今回はそんなことを考えながら、`Rails（管理画面＆API）`＋`Vue.js＆Nuxt.js(SPA）`という構成のアプリを作ってみました。題材は、ちょうど作り直したいと考えていた自分のポートフォリオサイトです。[^portfolio]

[^portfolio]: 初代は静的サイトとして構築したので、コンテンツを更新するときにサイト自体をデプロイし直す必要がありました。今度はDBと連携させて、コンテンツの更新を容易にしようと考えています。ちなみに初代を構築した時の記事はこちら。https://qiita.com/shozzy/items/dadea4181d6219d2d326

以下の役割分担で作っていきます。

|                       | Rails(View) | Rails(API) | Vue＆Nuxt |
|:---------------------:|:-----------:|:----------:|:--------:|
| コンテンツの表示（一般公開）  |    ○       |   ○        |    ○     |
| コンテンツの編集（管理者限定） |   ○       |   －        |    －     |


この記事では、Rails側で画面とAPIを作成するところまで掲載します。
SPA側の作成と、本番環境へのデプロイは、別途記事化します。

# Rails側の準備
Railsに持たせる機能がAPIだけだったら`rails new projectname --api`で良いのですが、これだとview関連のコードが作成されません。
今回は、管理画面は「Railsのいいとこ」を活かして作りつつ、SPA向けのAPIも作るので、このオプションは使わずに進めていきます。

以下、Rails5.2の環境が作成済みである前提です。[^rails6]

[^rails6]: 少し前にRails6がリリースされていますが、Rails5.xで開発を進めてきたのでまだバージョンアップしていません。

## rails new
まずは普通に rails new

```
$ rails new portfolio-rails
$ cd portfolio-rails
```

## deviseを導入
ユーザ管理にはdeviseを使います。こういった「よく使う機能を手軽に実現できるgem」が充実しているのも「Railsのいいとこ」と思います。

```file:Gemfile
gem 'devise'
```

```console:console
$ bundle install
$ rails g devise:install
```

続けて、`rails g devise:install`をした時に表示される`Some setup you must do manually if you haven't yet:`の下に書かれている1〜3を実施します。

```ruby:config/environments/development.rb
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
```

```ruby:config/routes.rb
  root to: "home#index"
```

```erb:app/views/layouts/application.html.erb
    <p class="notice"><%= notice %></p>
    <p class="alert"><%= alert %></p>
```


```console:console
$ rails g devise user
$ rails db:migrate
$ rails g devise:views
$ rails g devise:controllers users
```

この時点で、usersテーブルには以下の列が生成されています。

- email
- encrypted_password
- reset_password_token
- reset_password_sent_at
- remenber_created_at


ここで一度`rails s`して最低限の動作を確認しました。
<img width="406" alt="スクリーンショット 2019-09-22 22.49.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/f80e33d7-d8b9-c802-36e7-da8038e33dec.png">

大丈夫ですね。

すでにdeviseを入れてあるので、http://localhost:3000/users/sign_in からログイン画面にアクセスできます。
<img width="210" alt="スクリーンショット 2019-09-22 22.40.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/07784e47-c295-02be-3e5c-81bc35d98be3.png">

この時点では、サインアップしようとしても roots.rb に記述した root to: "home#index" をまだ作成していないので、Routing Errorになります。

routes.rbを書き換えます。

```routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    sessions: 'users/sessions'
  }
end
```

これで、サインアップしようとした際のRouting Errorはなくなりました。

ユーザ管理（認証・認可）周りはまだやることがありますが、一旦置いておき、他の作業に進みます。



## Githubへ初期コミット
生成されたコードや設定への変更点が増える前に、Githubへの初期コミットをしておきます。
Github側でリポジトリを作成し、そこの指示通りに作業します。
今回は「portfolio-rails」というリポジトリにしました。
https://github.com/shozzy/portfolio-rails


```
$ git init
$ git add README.md
$ git commit -m "first commit"
$ git remote add origin 
https://github.com/shozzy/portfolio-rails.git
$ git push -u origin master
```

これだけだとREADME.mdだけがpushされた状態なので、続けてここまでに生成されたコードや設定ファイルもpushしようと思いますが、その前に`.gitignore`の内容を見直しておきます。
正直詳しくないので、 gitignore.io で取得した内容をそのまま適用します。
https://www.gitignore.io/api/rails

これで公開してはいけないファイルを誤って公開してしまう可能性は低減できたでしょう。


それでは、コミットを実施します。[^commit]

[^commit]: ここまではmasterに直接コミットしていますが、この先はそういうことはしません。


```
$ git add .
$ git commit -m "second commit"
$ git push
```


## コンテンツ用のmodel, view, controllerを作成
ここからは、ポートフォリオのコンテンツ用のmodel, view, controllerを作成します。
今回は、scaffoldを使って、最低限のCRUDをざっくり作成してしまいます。

```
$ rails g scaffold Content title:String detail:String
$ rails db:migrate
```

`rails s`して確認すると、エラーになりました。
<img width="928" alt="スクリーンショット 2019-09-24 0.26.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/0bc027c2-8bf0-0aee-ab57-92a05cd4cf63.png">

マイグレーションはしたのですが、、、

と思ってコンソールを見直したら、マイグレーションファイルの中にtypoがありました。フィールドの型を`string`と書くべきところ、うっかりキャメルケースで`String`と書いていました。


```
NoMethodError: private method `String'
```

マイグレーションに失敗した状態なので、マイグレーションファイル[^2]を修正してから再度マイグレーションします。

[^2]: ここでは db/migrate/20190923152109_create_contents.rb でした。

```
$ rails db:migrate
== 20190923152109 CreateContents: migrating ===================================
-- create_table(:contents)
   -> 0.0015s
== 20190923152109 CreateContents: migrated (0.0016s) ==========================

```

成功しました。


`rails s`

まだ中身のデータは入っていませんが、scaffoldで作成した画面が表示できました。

<img width="315" alt="スクリーンショット 2019-09-24 0.36.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/29b51481-1c40-d18f-df4e-f2bbbdd718d0.png">


新機能を作成したので、featureブランチを作成して、そこにpushしておきます。


```
$ git checkout -b create-contents
$ git add .
$ git commit -m "create contents scaffold"
$ git push origin create-contents
```

## 試しに動かしてみる
一応動くものができているはずなので、試しに動かしてみましょう。ここでは、データを3件登録してみました。
scaffoldのままなのでドシンプルですが、正しく画面表示できています。
<img width="398" alt="スクリーンショット 2019-09-25 12.20.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/fa4077ed-4187-240c-cd16-b82d71ac7b1b.png">


## 最低限のテスト（RSpec）
APIを作る前に、ここまでの内容に対して最低限のテストを書いておきます。

RSpecとFactoryBotを導入します。

```file:Gemfile
group :test do
  # 中略
  gem 'rspec-rails', '~> 3.8'
  gem 'factory_bot_rails', '~> 5.1.0'
end
```

```console:console
$ bundle install
$ rails g rspec:install
```
これにより、specフォルダが作成されます。

minitest用のフォルダを削除します。

```
$ rm -r ./test
```

Headless Chromeを使用するように設定を入れます。

```file:spec/spec_helper.rb
require 'capybara/rspec'

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
end

```

下記設定がデフォルトではコメントアウトされていますが、コメントを外しておきます。

```file:spec/rails_helper.rb
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
```

```console:console
$ mkdir ./spec/factories
```

./spec/factories/contents.rb を作成します。

```file:./spec/factories/contents.rb
FactoryBot.define do
  factory :content do
    title { 'テストタイトル' }
    detail { 'テストコンテンツの明細です。' }
  end
end
```

```console:console
$ mkdir ./spec/system
```

./spec/system/contents_spec.rb を作成します。

```file:./spec/system/contents_spec.rb
require 'rails_helper'

describe 'コンテンツ管理機能', type: :system do
  describe '一覧表示機能' do
    context '1件だけデータがある場合' do
      before do
        # コンテンツを1件作成
        FactoryBot.create(:content, title:"テストコンテンツ1", detail:"コンテンツ1の明細")
        visit contents_path
      end
      it '1件のコンテンツが表示される' do
        # 表示内容を確認
        expect(page).to have_content 'テストコンテンツ1'
      end
    end
  end
end
```

```console:console
$ bundle exec rspec spec/system/contents_spec.rb
Capybara starting Puma...
* Version 3.12.1 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:55559
2019-09-28 15:15:03 WARN Webdrivers Driver caching is turned off in this version, but will be enabled by default in 4.x. Set the value with `Webdrivers#cache_time=` in seconds
.

Finished in 5.84 seconds (files took 2.37 seconds to load)
1 example, 0 failures
```

テストは無事にパスしました。

``Set the value with `Webdrivers#cache_time=` in seconds``のWARNが気になるので、下記対処をしておきます。

```console:console
$ mkdir ./spec/support
```

```file:spec/support/javascript_driver.rb
# During the cache time, Webdrivers won't check to update Chrome.
Webdrivers.cache_time = 1.month.to_i
```

WARNが出なくなりました✨

```console:console
$ bundle exec rspec spec/system/contents_spec.rb
Capybara starting Puma...
* Version 3.12.1 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:55767
.

Finished in 2.39 seconds (files took 2.34 seconds to load)
1 example, 0 failures
```

## APIを生やす
さて、ここからようやく本題です。APIを生やして行きます。

元々
`http://localhost:3000/contents.json`
へアクセスしたらJSONで結果が返ってきますが[^3]、ここではせっかくなのでもう少しAPIっぽく、
`http://localhost:3000/api/contents`
でアクセスしたらJSONで結果が返ってくるようにしてみます。

[^3]: https://qiita.com/ttiger55/items/d144b8094d61b70955bf にあるように、デフォルトのJSON生成機構では速度が遅いようですが、それは今後の課題としてここでは触れません。


routes.rbに以下の設定を追加します。APIからはindexとshowのアクションだけを呼べるようにしたいので、resouresは使わず、個別に設定しています。

```file:routes.rb
  scope '/api' do
    get '/contents', to: 'contents#index', defaults: { format: :json }
    get '/contents/:id', to: 'contents#show', defaults: { format: :json }
  end
```

scope を設定することで、URLに`/api`が入っていても`/api/contents`ではなく`/contents`にアクセスしたかのような動きになり、
各ルーティングの defaults に format の設定を入れることで、`/contents.json`ではなく`/contents`と書くだけでJSONが自動的にフォーマットとして指定されるようになります。
<img width="492" alt="スクリーンショット 2019-10-05 15.40.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/3de926a0-8904-75c7-b929-9717b6072ea2.png">

同様に、特定のコンテンツの明細も以下のように取得できます。[^4]

[^4]: このサンプルでは、一覧に全ての情報が含まれているので、明細を取得しても意味がありません。むしろ、URLなど不要な情報も含んでいるので、必要なデータだけ返すように改善が必要ですね。
<img width="516" alt="スクリーンショット 2019-10-06 14.46.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/7658edf9-8341-84a3-76e7-1e2740395e6c.png">



たったこれだけで、APIを作ることができました。

なお、今回は一般公開する機能だけをAPI化したので、APIキーによるアクセス制限は実施していません。
編集機能や特定ユーザだけに公開する情報をAPI化する時は、何らか[^apiauth]のアクセス制御を組み込む必要があります。

[^apiauth]: APIキーとリファラの組み合わせをチェックするとか。

## 編集系の機能をログイン必須にする
今回は自分以外は編集できないようにする計画ですが、ここまでの内容のままでは誰でもコンテンツを編集できてしまいます。
最初にdeviseを入れてありますので、それを利用してログイン状態でなければ追加・編集・削除を実施できないようにします。

controllerの`before_action`を使って、ログイン済みの場合だけ表示を認可します。
現時点では複雑な権限設定は持たせていないので、単純に「ログイン済みであれば誰でもOK」という仕組みです。[^auth]

[^auth]: 自分用のアカウントだけ発行する想定なのでこれで十分という判断です。アカウントごとに権限レベルによる画面制御を細かく掛けるなら、もっとしっかり作り込む必要があります。

contentsは、indexアクションは誰でもOKなので、それ以外のアクションに認可をかけています。

```ruby:contents_controller.rb
  before_action :authenticate_user!, only: [:show, :new, :edit, :create, :update, :destroy]
```

認可されていないアクションを実行しようとすると、ログイン画面にリダイレクトされます。
<img width="344" alt="スクリーンショット 2019-11-30 16.09.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/2f91ac8b-0f59-3012-a4d4-4fdb8472f5cb.png">


## masterブランチへマージする
最低限ではありますが、作りたかった機能が出来上がったので、自分にプルリクを出してmasterブランチへマージします。

まず、テストを流して問題が発生していないことを確認。

```
$ bundle exec rspec spec/system/contents_spec.rb
（中略）
Finished in 4.7 seconds (files took 1.95 seconds to load)
1 example, 0 failures
```

featureブランチにcommit&push。[^push]

```
$ git add .
$ git commit -m "add auth to contents"
$ git push
```

[^push]: 実際には、featureブランチにはもう少しこまめにコミット＆プッシュしていました。１機能分の進捗があった時と、作業が途切れるタイミングで。


GitHub側で、プルリクエストを出します。
<img width="758" alt="スクリーンショット 2019-12-01 15.12.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/9c08d841-86f7-51d6-76c5-b274cf72d5a6.png">


マージします。
<img width="768" alt="スクリーンショット 2019-12-01 15.16.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/a2b2658c-8cc8-2b0b-e4d0-73c9712a4477.png">

これで、masterブランチに開発内容を入れることができました。


最後にローカル側で、今後の作業に備えてmasterブランチをcheckoutしておきます。うっかりfeatureブランチからさらに別のfeatureブランチを派生させないために。個人開発なので、master＋feature１本ずつで運用する方針です。

```
$ git fetch
$ git checkout master
Switched to branch 'master'
Your branch is behind 'origin/master' by 8 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)
$ git pull
```



# まとめ
この記事では、contentsの編集画面をRailsのscaffold機能でサクッと作った上で、contentsの一覧と詳細をREST API経由でJSONとして取得できるところまでを作りました。

次の記事（鋭意制作＆執筆中）では、Vue.js＆Nuxt.jsで作成したSPAから、このAPIを叩いて取得した情報をいい感じに表示するところを作ります。

自己最長記事なので推敲にかなり時間をかけましたが、おかしいところがありましたら教えて頂けますと幸いです。


# 参考にした書籍・Webサイト
執筆者の皆様、本当にありがとうございます！

- 「現場で使える Ruby on Rails 5 速習実践ガイド」https://www.amazon.co.jp/dp/4839962227
- https://www.gitignore.io/api/rails
- https://railsguides.jp/routing.html
- https://bokunonikki.net/post/2018/0804_rails5_devise/
- https://qiita.com/Hal_mai/items/350c400e8763ce0487a3
- https://qiita.com/tatsurou313/items/c923338d2e3c07dfd9ee
- https://bloggie.io/@kinopyo/migrate-from-chromedriver-helper-to-webdrivers
- https://qiita.com/Kokudori/items/d2bcb6fd24c662e73a33
- https://qiita.com/ttiger55/items/d144b8094d61b70955bf
- https://qiita.com/ebi_death/items/3912630e32268c9cce46
- https://qiita.com/tobita0000/items/866de191635e6d74e392
- https://solodev.io/git-flow/