9
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-30

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フォーマットに対応しなければ・・・
  5. JSON:APIフォーマットのままだと、Nuxtで扱いずらく、ある程度整形が必要だと知る。でも自力でガチャガチャ整形したくない。
  6. 便利なプラグインはないかなー

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

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

対象者

  • 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をご覧ください。

JSON:APIフォーマット例

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

test.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をご覧ください。

やりたいことの概要

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

名称未設定.001.png

APIサーバ側(Ruby on Rails)の処理

Fast JSON APIとは

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

導入については、こちらの記事が参考になります。
詳細が知りたい場合には、リファレンスをご覧ください。

各処理の実装

シリアライザーの設定

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)に変換してくれるプラグインです。
詳細が知りたい場合には、リファレンスをご覧ください。

他にも色々な実装方法があるみたいで、
JSON:APIのHPに各言語毎の実装方法(プラグイン)について記載されています。
jsonapi-vuexとどっちにしようか迷いましたが、
APIサーバからのレスポンスをその都度vuexに保存するのが嫌だったため、Devour JSON-API 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とモデル間のリレーションの設定をします。
リレーションで設定したモデル(ここではshopclerk_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"
    }
  ]
},
{
    ・
    ・
    ・
}

終わりに

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

9
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?