【RailsAPI×Docker】シェルで手軽に環境構築 & Flutterでも動作確認

初心者の方でも進められるよう手順は丁寧してあります。

# やること

- **RailsAPI × Docker での環境構築**
- **RailsAPIを軽く実装**
- **APIクライアントからAPIを叩く**（PostmanやFlutterアプリから）※Flutterは任意で

![スクリーンショット 2020-09-13 13.42.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305969/512acedc-3167-7d02-23f9-9af4ae8166cd.png)

APIコンテナ起動のシェルだけ見たい方 → [こちら](https://qiita.com/drafts/942ad5fe5c13f70a19c5/edit#1-2-%E3%82%B7%E3%82%A7%E3%83%AB%E3%81%A7railsapi%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%83%93%E3%83%AB%E3%83%89)
一応 [RailsAPIのソースコード](https://github.com/TsuboyaTaiki/rails-api-handson)、[Flutterのソースコード](https://github.com/TsuboyaTaiki/flutter_handson)も用意（※いじり倒してる可能性あり）

### 手順

- API側のDockerコンテナ起動まで
  - ディレクトリ作成
  - シェルでRailsAPIコンテナビルド
  - コンテナ起動&確認
- RailsAPI実装
  - User モデル作成&DBテーブル用意
  - User のコントローラ作成
  - ルーティング設定
- PostmanでAPIの動作を確認
  - Postmanインストール
  - `GET`でDBのUserリソースを取得
  - `POST`でDBのUserリソースを追加
- FlutterでAPIを叩く
  - プロジェクト作成
  - Http通信を行えるようにする
  - main.dartを編集

### 前提
- AndroidStudioでFlutterアプリケーションを起動した状態にする
 - SDKなどをダウンロードしてシミュレータを起動できる
 - ※ Flutterセットアップは[こちらの記事](https://qiita.com/henjiganai/items/3419ca49a3e36bc59828)で基本全部いけました（3~40分はかかります）
- Docker, docker-compose のコマンドが使える（`$ docker-compose -v`が動けばOK）

# 1. API側のDockerコンテナ起動まで

RailsAPIのDockerコンテナを作っていきます。

ただ、1からコマンドを打っていくのは非効率なので、基本的なところはシェルにまとめてあります。

## 1-1. ディレクトリ作成

`app_name`にプロジェクト名を当てはめてディレクトリを用意します。

```bash
$ mkdir app_name  # APIアプリを置くディレクトリ用意
$ cd app_name
```

## 1-2. シェルでRailsAPIコンテナビルド

以下がAPIコンテナのビルドまでやってくれるシェルです。([参考にした記事](https://masaki.blog/rails6-on-docker/))

<details><summary>**サンプルAPI立ち上げ用のシェル**</summary><div>


```bash:set_up_rails.sh
#!/bin/bash

#config setting#############
APP_NAME="app_name"            # ← 自由に変更してください（ディレクトリ名と一緒がいいかも）
MYSQL_PASSWORD="password"      # ← 自由に変更してください
###########################

echo "docker pull ruby2.6.6"
docker pull ruby:2.6.6

echo "docker pull mysql:5.7"
docker pull mysql:5.7

echo "docker images"
docker images

echo "make Dockerfile"

cat <<EOF > Dockerfile
FROM ruby:2.6.6
ENV LANG C.UTF-8
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
#yarnのセットアップ
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
ENV PATH /root/.yarn/bin:/root/.config/yarn/global/node_modules/.bin:\$PATH
# 作業ディレクトリの作成、設定
RUN mkdir /${APP_NAME}
ENV APP_ROOT /${APP_NAME}
WORKDIR \$APP_ROOT
# ホスト側（ローカル）のGemfileを追加する
ADD ./Gemfile \$APP_ROOT/Gemfile
ADD ./Gemfile.lock \$APP_ROOT/Gemfile.lock
# Gemfileのbundle install
RUN bundle install
ADD . \$APP_ROOT
# gem版yarnのuninstall rails6でエラーになるため
RUN gem uninstall yarn -aIx
#webpackerの設定
#RUN rails webpacker:install
EOF

echo "make Gemfile"
cat <<EOF > Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem 'rails', '~> 6.0.3.2'
EOF

echo "make Gemfile.lock"
touch Gemfile.lock

echo "make docker-compose.yml"
cat <<EOF > docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_DATABASE: root
    ports:
      - '3306:3306'
  api:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/${APP_NAME}
    ports:
      - '3000:3000'
    links:
      - db
EOF

echo "docker-compose run api rails new . --api --force --database=mysql --skip-bundle"
docker-compose run api rails new . --api --force --database=mysql --skip-bundle

echo "docker-compose run api bundle exec rails webpacker:install"
docker-compose run api bundle exec rails webpacker:install

docker-compose build

# fix config/database.yml
echo "fix config/database.yml"
cat config/database.yml | sed "s/password:$/password: ${MYSQL_PASSWORD}/" | sed "s/host: localhost/host: db/" > __tmpfile__
cat __tmpfile__ > config/database.yml
rm __tmpfile__

echo "docker-compose run api rails db:create"
docker-compose run api rails db:create

```

</div></details>

エディタからプロジェクト直下に新規ファイルとして作成＆↑をコピペし、`set_up_rails.sh`などとして保存しましょう。「自由に変更してください」となっている`app_name`などは合わせて編集してください。

保存が終わったら以下のようにシェルの権限を変更し、実行してみましょう（5分くらいかかります）。

```bash
$ chmod 755 set_up_rails.sh  # 権限の変更
$ ./set_up_rails.sh          # セットアップのシェル実行
docker pull ruby2.6.6
2.6.6: Pulling from library/ruby
57df1a1f1ad8: Pull complete
71e126169501: Pull complete
1af28a55c3f3: Pull complete
・
・
・
```

シェルの実行が終わったら、`$ docker-compose ps`でコンテナの状態を確認してみましょう。以下のようになっていれば問題ないです。

```bash
$ docker-compose ps
     Name             Command       State       Ports
----------------------------------------------------------
app_name_db_1     docker-           Up      0.0.0.0:3306->
                  entrypoint.sh             3306/tcp,
                  mysqld                    33060/tcp
```

## 1-3. コンテナ起動 & 確認

次に、シェルでビルドしたコンテナを起動します。

```bash
$ docker-compose up
rails-api-handson_db_1 is up-to-date
Creating rails-api-handson_api_1 ... done
Attaching to rails-api-handson_db_1, rails-api-handson_api_1
db_1   | 2020-09-12 08:27:00+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.31-1debian10 started.
・
・
・
api_1  | * Environment: development
api_1  | * Listening on tcp://0.0.0.0:3000
api_1  | Use Ctrl-C to stop    # ← これが出たらOK！
```

Dockerコンテナのログが大量に出ますが、最後の**`Use Ctrl-C to stop`**が出ればコンテナが起動しているのでOKです。

コンテナの起動が確認できたので、http://localhost:3000 にアクセスし、ちゃんとRailsとしてレスポンスを返してくれるか確認しましょう。以下のようになっていればAPIコンテナの動作は大丈夫です。

![スクリーンショット 2020-09-12 17.38.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305969/ec454662-99f2-d585-cedd-5bbe4a07cf7f.png)

# 2. RailsAPI実装

動作確認用なので単純なユーザデータにしましょう。今回は、

**`User ( id, name, email )`**

みたいなリソースで実装してみます。

## 2-1. Userモデル作成 & DBテーブル用意

Userモデルとテーブルを用意します。

ここだけ**マイグレーションを使う方法**と**Ridgepoleを使う方法**に分けて紹介します（どちらも結果として同じモデル、スキーマが反映されます）。

[Ridgepole](https://github.com/winebarrel/ridgepole)は、マイグレーションを使わずにスキーマを管理できるツールです。僕はこちらで進めていきますが、普段からマイグレーションを使っている方であればそちらで問題ありません。

### パターン1） マイグレーションでモデル&テーブル作成

コンテナに入り、ジェネレータでUserモデルを作成。

```bash
$ docker-compose exec api bash     # コンテナに入る
＃ rails g model user name:string email:string
# ↑ 出来なければ以後はBundle経由でやってみましょう（ ＃ bundle exec rails .... みたいな ）
＃ exit                             # コンテナから出る（以後はコンテナの出入りは明記しません。
                                   #              コマンドラインの「$」や「＃」で区別します。）
$
```

あとはマイグレーションをかけるだけです。

```bash
＃ rake db:migrate
```

### パターン2） Ridgepoleでモデル&テーブル作成

マイグレーションをスキップしてモデルを作成。

```bash
＃ rails g model user --skip-migration
```

プロジェクト直下にある`Gemfile`に`gem 'ridgepole', '>= 0.8.0'`を追記します。mysql2の下とかで問題ないでしょう。

```ruby:Gemfile
gem 'mysql2', '>= 0.4.4'     # 元々ある
gem 'ridgepole', '>= 0.8.0'  # これを追加
```

追記したgemをインストール。

```bash
＃ bundle install
```

次に`db/`のなかに、`Schemafile.rb`というファイルを作成します（Ridgepoleではスキーマをここで一元管理する）。

`db/Schemafile.rb`には以下のように書き、保存します。

```ruby:db/Schemafile.rb
create_table "users", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
  t.string "name"
  t.string "email"
  t.timestamps
end
```

ridgepoleのコマンドでスキーマファイルを反映。

```bash
＃ bundle exec ridgepole --config ./config/database.yml --file ./db/Schemafile.rb --apply
Apply `./db/Schemafile.rb`
-- create_table("users", {:options=>"ENGINE=InnoDB DEFAULT CHARSET=utf8"})
   -> 0.0716s
```

## 2-2. User のコントローラ作成

[こちらの記事](https://qiita.com/k-penguin-sato/items/adba7a1a1ecc3582a9c9)を参考に、URIにAPIのメジャーバージョン（v1.3なら1の部分）を含めます。ただこの記事だと、URIに`api`というネームスペースを使っています。

これは僕個人の意見ですが、URIに`api`と明記する方法ではなく、サブドメインなどで示すことを想定して実装します。

```
example.com/api/vi/users  # URIでapiだと示す
api.example.com/vi/users  # サブドメインで示す
```

では本題のコントローラ作成に戻ります。

コントローラの前に、メジャーバージョンを示す`v1/`を作成し、その中でusersコントローラを作成します。

```bash
$ mkdir app/controllers/v1
$ rails g controller v1::users
```

コントローラファイルの中身は以下の感じです。

```ruby:*/v1/users_controller.rb
class V1::UsersController < ApplicationController
  before_action :set_user, only: [:show, :update, :destroy]

  def index
    users = User.order(created_at: :desc)
    render json: { status: 'SUCCESS', message: 'Loaded user', data: users }
  end

  def show
    render json: { status: 'SUCCESS', message: 'Loaded the user', data: @user }
  end

  def create
    user = User.new(user_params)
    if user.save
      render json: { status: 'SUCCESS', data: user }
    else
      render json: { status: 'ERROR', data: user.errors }
    end
  end

  def destroy
    @user.destroy
    render json: { status: 'SUCCESS', message: 'Deleted the user', data: @user }
  end

  def update
    if @user.update(user_params)
      render json: { status: 'SUCCESS', message: 'Updated the user', data: @user }
    else
      render json: { status: 'SUCCESS', message: 'Not updated', data: @user.errors }
    end
  end

  private

  def set_user
    @user = User.find(params[:id])
  end

  def user_params
    params.require(:user).permit(:name, :email)
  end
end
```

## 2-3. ルーティング設定

`config/routes.rb`を以下のようにします。

```ruby:config/routes.rb
Rails.application.routes.draw do
  namespace 'v1' do
    resources :users
  end
end
```

↑が終わると、`rake routes`コマンドでルーティングが確認できるはずです。以下のようなルーティングが反映されていればOKです。

```bash
＃ rake routes
  Prefix Verb    URI Pattern               Controller#Action
v1_users GET     /v1/users(.:format)       v1/users#index
         POST    /v1/users(.:format)       v1/users#create
 v1_user GET     /v1/users/:id(.:format)   v1/users#show
         PATCH   /v1/users/:id(.:format)   v1/users#update
         PUT     /v1/users/:id(.:format)   v1/users#update
         DELETE  /v1/users/:id(.:format)   v1/users#destroy
```

# 3. PostmanでAPIの動作を確認

Postmanとは、WebAPIのテストクライアントサービスのひとつです。APIの動作確認にはこれを使います。

## 3-1. Postmanインストール

[公式サイト](https://www.postman.com/)で、Postmanをインストールします。前にインストールしたのであまり覚えていませんが、登録して進めていけばOKだった気がします...。

Postmanインストール後、まずはコンソールで適当にUserのレコードを用意しておきましょう。以下のような感じでいいかと思います。

```bash
＃ rails c
Loading development environment (Rails 6.0.3.3)
irb(main):001:0> User.create(name: "hoge", email: "hoge@example.com")
```

## 3-2. `GET`でDBのUserリソースを取得

Postmanで以下のように`GETメソッド`、`URI`を指定し、`Send`ボタンを押します。

すると、画面下のBodyで返ってきたレスポンスのBodyが確認できます。ここまでできていればAPIの動作確認としては成功です！

![スクリーンショット 2020-09-12 21.15.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305969/0e936bb4-8f3e-eb20-62c7-bf6cae8633cf.png)

## 3-3. `POST`でDBのUserリソースを追加

今度は以下のように、`POSTメソッド`を指定し、リクエストBodyの中にもデータを含めてみましょう。この時、形式が`JSON`になっているか確認しましょう（画像の"JSON"とオレンジになっているところ）。

成功すれば、登録されたUserのレコードがレスポンスBodyから確認出来ます。

![スクリーンショット 2020-09-12 21.21.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305969/17909f82-d368-ab68-a62c-2166a920aaf5.png)

ここで先ほどの`GETメソッド`でのアクセスをもう一度行ってみると、`POST`メソッドで追加したレコードも一緒に確認できます。

![スクリーンショット 2020-09-12 21.22.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305969/016dc07a-14bb-146d-a2b3-e489dd75afb9.png)


# 4. FlutterでAPIを叩く

### 結果こんな感じ

- アプリを起動したらDBにあるユーザのデータを表示
- 右下の`Add`ボタンでユーザデータの登録

動作確認なのでミニマムな感じです。

![スクリーンショット 2020-09-12 23.28.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305969/e80ab7d3-cb13-29d2-b99a-5660276a1ba2.png)

## 4-1. プロジェクト作成

**`File > New > New Flutter Project...`** からFlutterプロジェクトを作成します。iPhoneやAndroidのシミュレータ（エミュレータ）などが起動できていればOKです。

## 4-2. Http通信を行えるようにする

デフォルトだとhttpを行うパッケージのimportでエラーを起こすので少し操作が必要になります。

まず、`pubspec.yaml`の`dependencies:`に以下のように追記します。

```yaml:pubspec.yaml
# ~ 省略 ~
dependencies:
  http: ^0.12.0   # ← 追加する
  flutter:
    sdk: flutter
# ~ 省略 ~
```

すると、Android Studioのエディタの上の方に、以下のようなFlutterコマンドが出てきます。今回は**`Pub get`**を選択します。

もしこの先の作業でHttp周りのエラーが出るようであれば、僕が参考にした[こちらの記事](https://qiita.com/kenichiro-yamato/items/ce7d77e9c733c24024d8)も見てみるといいかも知れません。

![スクリーンショット 2020-09-12 22.35.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305969/4a39f36e-150c-0a89-67a3-c5e7ace50888.png)

## 4-3. main.dartを編集

動作確認をしたいだけであれば、`lib/main.dart`を以下のように書き換えれば動きます。

なお、僕はFlutterは初歩の初歩なので物申すことがあればコメントにお願いします。

```dart:lib/main.dart
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  MyApp({Key key}) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Future<GetResults> res;
  @override
  void initState() {
    super.initState();
    res = getUsers();
  }

  // postUser()でUserを登録。setState内で再度User一覧を取得
  void _postUser() {
    postUser();
    setState(() {
      res = getUsers();
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'APIをPOSTで呼び出しJSONパラメータを渡す',
      theme: ThemeData(
        primarySwatch: Colors.yellow,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('RailsAPI x Flutter'),
        ),
        body: Center(
          child: FutureBuilder<GetResults>(
            future: getUsers(),
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(
                    snapshot.data.message.toString()
                );
              } else if (snapshot.hasError) {
                return Text("${snapshot.error}");
              }
              return CircularProgressIndicator();
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: _postUser,
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

class GetResults {
  final String message;
  GetResults({
    this.message,
  });
  factory GetResults.fromJson(Map<String, dynamic> json) {
    var datas = '';
    json['data'].forEach((item) => datas += 'id: ' + item['id'].toString() + ', name: ' + item['name'] + ', email: ' + item['email'] + '\n');
    return GetResults(
      message: datas,
    );
  }
}

class PostResults {
  final String message;
  PostResults({
    this.message,
  });
  factory PostResults.fromJson(Map<String, dynamic> json) {
    return PostResults(
      message: json['message'],
    );
  }
}

class SampleRequest {
  final String name;
  final String email;
  SampleRequest({
    this.name,
    this.email,
  });
  Map<String, dynamic> toJson() => {
    'name': name,
    'email': email,
  };
}

Future<GetResults> getUsers() async {
  var url = 'http://127.0.0.1:3000/v1/users';
  final response = await http.get(url);
  if (response.statusCode == 200) {
    return GetResults.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed');
  }
}

Future<PostResults> postUser() async {
  var url = "http://127.0.0.1:3000/v1/users";                             // APIのURI
  var request = new SampleRequest(name: 'foo', email: 'foo@example.com'); // Userのパラメータ。自由に変更してOK
  final response = await http.post(url,
      body: json.encode(request.toJson()),
      headers: {"Content-Type": "application/json"});
  if (response.statusCode == 200) {
    return PostResults.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed');
  }
}
```

シミュレータを起動すると、、登録されたUserがきちんと返ってきてますね！

![スクリーンショット 2020-09-12 21.30.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305969/949b3640-44e3-61b7-8633-e0f0aa8fabdd.png)

右下のAddボタンを押すと、、`main.dart`で指定したUserデータが登録され、取得できているのも確認できました！

![スクリーンショット 2020-09-12 21.58.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305969/9b4ea004-3cf9-f4dd-571d-5c4142336c3a.png)

## 終わり
質問やご指摘があればコメントにお願いします。