APIサーバ（Ruby on Rails）で生成したJSON:APIフォーマットのデータをフロント（Nuxt.js）で扱う

# APIサーバ（Ruby on Rails）で生成したJSON:APIフォーマットのデータをフロント（Nuxt.js）で扱う

## はじめに

この記事は、APIサーバから受け取ったJSON:APIフォーマットのデータを
フロント側で簡単に取り扱う方法を説明しています。

なんでこんなことをしたくなったのか、経緯は以下のとおりです。

1. RailsからJSONを返却する際にActiveModel::Serializers(gem)を使っていたが、処理が遅い
2. NetflixのFast JSON API（gem）は使い勝手が「ほぼ同じ」かつ「処理が早い」らしいので使ってみよう ← 噂通り
3. Fast JSON APIはJSON:APIフォーマットに従って、JSONを生成し、返却することを知る
4. フロント側もJSON:APIフォーマットに対応しなければ・・・
4. JSON：APIフォーマットのままだと、Nuxtで扱いずらく、ある程度整形が必要だと知る。でも自力でガチャガチャ整形したくない。
5. 便利なプラグインはないかなー

Nuxt.jsでJSON:APIフォーマットのデータを扱う方法について、ネットで調べましたが、
日本語の記事が全くヒットしませんでした。（もしかしてこんなことをすること自体が異常なのかもしれない）
そこで、同じような悩みを抱える人が困らないように、ここに記事を残したいと思います。

なお、フロント側の実装部分が知りたい方は、後ろの章（フロント側（Nuxt.js）の処理）をご覧ください。
また、今回の例とは異なりますが、導入例を確認したい場合には、[GitHubリポジトリ](https://github.com/gonkunkun/sopick)をご覧ください。

## 対象者

- JSON:APIフォーマットに沿ってRailsとNuxt間のAPI通信をしたい人
- 特に、NuxtでのJSON:APIフォーマットの取り扱い方（データの整形）に悩んでいる人

## 開発環境と今回の主役となるプラグイン

- Rails 6.0.2
    - Fast JSON API
- @nuxt/cli v2.11.0
    - Devour JSON-API Client
    
## JSON:APIとは

API設計時にJSONのデータ構造に悩まなくてもいいように、「こんなフォーマットはいかがですか？」を
提唱してくれている組織です。
詳しくは、[HP](https://jsonapi.org/)をご覧ください。

### JSON:APIフォーマット例

例えば、以下のDBがあるとします。

![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186355/3523c823-e0da-abc3-4ece-d9f2c4fd1aa2.png)

モデル間のリレーションが貼られている前提で、RailsからfindでClerk（店員）一覧を取得してJSONを返却すれば、以下のようなフォーマットになるのではないでしょうか（例1）。

```  
{
  "id": 1,
  "name": "太郎",
  "shop": {
    "shop_id": 1,
    "shop_name": "ラーメン五郎"
  },
  "clerk_images": [
    {
      "image_id": 1,
      "image_url": "http://hogehoge.com"
    },
    {
      "image_id": 2,
      "image_url": "http://hugahuga.com"
    }
  ]
},
{
　　　　・
　　　　・
　　　　・
}

ここのフォーマットについては、Railsで直接出力して試したわけではないので、
間違いがあるかもしれません。参考程度で見てください。
```

これが、JSON:APIフォーマットの場合、以下のような出力となります（例2）。

```
{
  "data": [
    {
      "id": "1",
      "type": "clerk",
      "attributes": {
        "name": "太郎",
      },
      "relationships": {
        "shop": {
          "data": { "id": "1", "type": "shop" }
        },
        "clerk_images": {
          "data": [
            { "id": "1", "type": "clerk_image" },
            { "id": "2", "type": "clerk_image" }
          ]
        }
      }
    }
  "included": [
    {
      "id": "1",
      "type": "clerk_image",
      "attributes": {
        "image_url": "http://hogehoge.com"
      }
    },
    {
      "id": "2",
      "type": "clerk_image",
      "attributes": {
        "image_url": "http://hugahuga.com"
      }
    },
    {
      "id": "1",
      "type": "shop",
      "attributes": {
        "image_url": "http://hugahuga.com"
      }
    }
｝
```

JSON:APIフォーマットのポイントは以下のとおりです。
- shopやclerk_images等のrelationshipsについて、data部の中ではidとtypeの値のみ保持している
- relationships内のattributesを取得する場合には、idとtypeをキーとして、included部 or （今回は触れていないが）別途APIのエンドポイントから取得する

APIから受け取るデータの重複が減って、綺麗なJSONフォーマットとなります。
今回の例だと、データ量が少ないのであまりピンと来ないかもしれませんが、
データ量が多くなると、その分恩恵は大きくなるかと思います。

※本来であれば、APIへのエンドポイントを示すlinks等もありますが、
 説明の都合上割愛しております。興味がある方は[JSON:APIのHP](https://jsonapi.org/)をご覧ください。

## やりたいことの概要

ここまできて、ようやく本題に入ります。
以下はやりたいことの流れのイメージです。
それっぽい図を用意してますが、単純にフロントからAPIサーバにリクエストを飛ばして、
サーバがDBから値を持ってきて、JSON：APIフォーマットで返却するという流れです。

![名称未設定.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186355/c525d1ad-fd50-1821-66d1-52901f9cf408.png)


## APIサーバ側（Ruby on Rails）の処理
### Fast JSON APIとは

Netflix製のシリアライザです。DBから取得した値をJSON：APIフォーマットに変換してくれます。
ActiveModel::Serializersよりも実行速度が早いです。使い勝手はほぼ同じで、移行も簡単です。

導入については、[こちらの記事](https://tech.dely.jp/entry/2019/12/02/193000)が参考になります。
詳細が知りたい場合には、[リファレンス](https://github.com/Netflix/fast_jsonapi)をご覧ください。

### 各処理の実装

#### シリアライザーの設定

``` app/serializers/clerk_serializer.rb
class ClerkSerializer
  include FastJsonapi::ObjectSerializer
  attributes :id,
             :shop_id,
             :image_id,
             :name
  has_one  :shop
  has_many :clerk_images
end

```

``` app/serializers/shop_serializer.rb
class ShopSerializer
  include FastJsonapi::ObjectSerializer
  attributes :id,
             :shop_name,
  has_many   :clerks
end
```

``` app/serializers/clerk_image_serializer.rb
class ClerkImageSerializer
  include FastJsonapi::ObjectSerializer
  attributes :id,
             :image_url
end
```

#### コントローラー

コントローラの生成やルーティングの設定は別途実施してください。

``` app/controllers/clerks_controller.rb
class ClerksController < ApplicationController
  def index
    clerks = Clerk
              .includes(:clerk_images).includes(:shop)
    
    # join先のテーブルのattributeを戻り値に付与する
    options = {}
    options[:include] = [:clerk_images, :shop]

    render json: ClerkSerializer.new(clerks, options).serialized_json
  end
end
```

## フロント側（Nuxt.js）の処理
### Devour JSON-API Clientとは

JSON:APIフォーマット(例2)をフロント側で扱いやすいフォーマット（例1）に変換してくれるプラグインです。
詳細が知りたい場合には、[リファレンス](https://www.npmjs.com/package/devour-client)をご覧ください。

他にも色々な実装方法があるみたいで、
[JSON:APIのHP](https://jsonapi.org/implementations/)に各言語毎の実装方法（プラグイン）について記載されています。
[jsonapi-vuex](https://github.com/mrichar1/jsonapi-vuex)とどっちにしようか迷いましたが、
APIサーバからのレスポンスをその都度vuexに保存するのが嫌だったため、[Devour JSON-API Client](https://www.npmjs.com/package/devour-client)にしました。

その他、実装にあたって参考にした記事
http://alexandrubucur.com/blog/2019/simple-jsonapi-plugin-for-nuxt.js/


### 各処理の実装

#### インストール

```
yarn add devour-client
```

#### ソースコード

``` （一部抜粋）index.vue
<template>
  <v-container fluid>
    <v-row>
      <clerk-list :clerks="clerks" :pagination="pagination" />
    </v-row>
  </v-container>
</template>

<script>
import JsonApi from "devour-client"

export default {
  data: () => ({
    clerks: []
  }),
  created() {
    this.fetch()
  },
  methods: {
    async fetch() {
      const jsonApi = new JsonApi({ apiUrl: "http://localhost:3031" })
      // フロント側の受け皿を定義します（Railsでいうところのモデル）
      // clerk
      jsonApi.define("clerk", {
        id: "",
        shop_id: "",
        image_id: "",
        name: "",
        // リレーションの設定
        shop: {
          // shopが1
          jsonApi: "hasOne",
          type: "shop"
        },
        clerk_images: {
          // clerk_imagesが多
          jsonApi: "hasMany",
          type: "clerk_image"
        }
      })
      // shopのattribute
      jsonApi.define("shop", {
        shop_name: ""
      })
      // clerk_imageのattribute
      jsonApi.define("clerk_image", {
        image_url: ""
      })

      let { data, meta } = await jsonApi.findAll("clerks")
      console.log(data)
      this.clerks = data
      // 本記事では触れてませんが、ページネーションのためにAPIサーバから取得していました
      this.pagination = meta.pagination
    }
  }
}
</script>
```

いくつか重要そうな部分だけ見ていきます。

#### 受け皿となるモデルの設定

```
      jsonApi.define("clerk", {
        id: "",
        shop_id: "",
        image_id: "",
        name: "",
        // リレーションの設定
        shop: {
          // shopが1
          jsonApi: "hasOne",
          type: "shop"
        },
        clerk_images: {
          // clerk_imagesが多
          jsonApi: "hasMany",
          type: "clerk_image"
        }
      })
```

主となるモデルのattributeとモデル間のリレーションの設定をします。
リレーションで設定したモデル（ここでは`shop`と`clerk_images`）は、追加で以下のように設定が必要です。

```
      // shopのattribute
      jsonApi.define("shop", {
        shop_name: ""
      })
      // clerk_imageのattribute
      jsonApi.define("clerk_image", {
        image_url: ""
      })
```

#### APIサーバへのリクエスト送受信

```
let { data, meta } = await jsonApi.findAll("clerks")
```
決められた書き方に従うと、勝手にAPIサーバへリクエストするためのURLを作ってくれます。

上記の他にも`find one`, `create`, `update`等を使用でき、リクエストパラメータを付与することもできます。

```
// To find many with filters...
jsonApi.findAll('post', {page: 2})
```
例えば、上のように書くと、リクエストパラメータを付与できます。

詳細は、以下をご覧ください。
https://www.npmjs.com/package/devour-client

### 結果

APIサーバからの返却値（data）を確認すると、
例1であげたような以下のJSONのフォーマットに変換できていることが確認できます。

```  
{
  "id": 1,
  "name": "太郎",
  "shop": {
    "shop_id": 1,
    "shop_name": "ラーメン五郎"
  },
  "clerk_images": [
    {
      "image_id": 1,
      "image_url": "http://hogehoge.com"
    },
    {
      "image_id": 2,
      "image_url": "http://hugahuga.com"
    }
  ]
},
{
　　　　・
　　　　・
　　　　・
}

```

## 終わりに

２〜３日悩んでいたので、他の人はもっと早く解決できることを祈ります。
なお、記事用にモデル名等を直しているので、入力ミスがあって、ただ単にコピペだと動かないかもしれません。その際はお許しください。
そもそも、フロントとサーバ間のデータ送受信フォーマットは何が最適なのか...
今回はJSON:APIフォーマットを参考にしましたが、他にベストプラクティス的なものがあれば、共有していただければありがたいです。