APIサーバ(Ruby on Rails)で生成したJSON:APIフォーマットのデータをフロント(Nuxt.js)で扱う
はじめに
この記事は、APIサーバから受け取ったJSON:APIフォーマットのデータを
フロント側で簡単に取り扱う方法を説明しています。
なんでこんなことをしたくなったのか、経緯は以下のとおりです。
- RailsからJSONを返却する際にActiveModel::Serializers(gem)を使っていたが、処理が遅い
- NetflixのFast JSON API(gem)は使い勝手が「ほぼ同じ」かつ「処理が早い」らしいので使ってみよう ← 噂通り
- Fast JSON APIはJSON:APIフォーマットに従って、JSONを生成し、返却することを知る
- フロント側もJSON:APIフォーマットに対応しなければ・・・
- JSON:APIフォーマットのままだと、Nuxtで扱いずらく、ある程度整形が必要だと知る。でも自力でガチャガチャ整形したくない。
- 便利なプラグインはないかなー
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があるとします。
モデル間のリレーションが貼られている前提で、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フォーマットで返却するという流れです。
APIサーバ側(Ruby on Rails)の処理
Fast JSON APIとは
Netflix製のシリアライザです。DBから取得した値をJSON:APIフォーマットに変換してくれます。
ActiveModel::Serializersよりも実行速度が早いです。使い勝手はほぼ同じで、移行も簡単です。
導入については、こちらの記事が参考になります。
詳細が知りたい場合には、リファレンスをご覧ください。
各処理の実装
シリアライザーの設定
class ClerkSerializer
include FastJsonapi::ObjectSerializer
attributes :id,
:shop_id,
:image_id,
:name
has_one :shop
has_many :clerk_images
end
class ShopSerializer
include FastJsonapi::ObjectSerializer
attributes :id,
:shop_name,
has_many :clerks
end
class ClerkImageSerializer
include FastJsonapi::ObjectSerializer
attributes :id,
:image_url
end
コントローラー
コントローラの生成やルーティングの設定は別途実施してください。
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
ソースコード
<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"
}
]
},
{
・
・
・
}
終わりに
2〜3日悩んでいたので、他の人はもっと早く解決できることを祈ります。
なお、記事用にモデル名等を直しているので、入力ミスがあって、ただ単にコピペだと動かないかもしれません。その際はお許しください。
そもそも、フロントとサーバ間のデータ送受信フォーマットは何が最適なのか...
今回はJSON:APIフォーマットを参考にしましたが、他にベストプラクティス的なものがあれば、共有していただければありがたいです。