MAGAZINE
ルーターマガジン
Rails - ActiveModelSerializers gemでサクサクAPI開発
こんにちは、学生エンジニアのkanekoです。
皆さんはRuby on RailsのAPIモードを使ってますか?かの有名なRailsチュートリアルでは、erbなどのテンプレートエンジンを用いてフロントエンドもRails側で行う開発手法を紹介していますが、APIモードでの開発まではカバーしていません。昨今はフロントエンドはJSフレームワークに任せてバックエンドはJSONを返すだけ、というような構成が主流になりつつあります。
そこで、今回はRailsのAPIモードを用いた開発の簡単なチュートリアルをお送りしたいと思います。
出力するJSONはactive_model_serializersというgemでシリアライズ(データの整形)するので、そちらの方も解説します。
Ruby on Rails APIモードの基本
ここからは実際にAPIモードを使って簡単なAPIを作っていきます。
環境
- Ruby on Rails 5.2.4.1(必ずAPIモードが実装されている5.0以上のものを使う)
- ruby 2.5.3
~ $ rails _5.2.4.1_ new sample_app --api
--api
オプションをつけることでAPI開発専用のモードとなります。
モデルの作成
次はモデルを作っていきます。今回はシンプルなアプリにしたいのでエンティティはユーザーと投稿のみ、ユーザーは投稿を複数所持する という関係にします。
~ $ cd sample_app
sample_app $ rails g model User name:string
sample_app $ rails g model Post text:string user:references
sample_app $ rails db:create
sample_app $ rails db:migrate
これでUserモデルとPostモデルが作成できました。ちなみにPostモデルの作成時に指定した
user:references
で、以下のように外部キーとなるuser_idカラムがインデックス付きで作成されます。
db/migrate/2020~_create_posts.rb
class CreatePosts < ActiveRecord::Migration[5.2]
def change
create_table :posts do |t|
t.string :text
t.references :user, foreign_key: true
t.timestamps
end
end
end
最後にモデル同士の関連付けを行います。Userが複数のPostを持つ、という一対多の関連なのでUserにhas_many、 Postにbelongs_toの関連付けを行っていきます。が、references型でuserを指定した時点でPostのbelongs_toは自動生成されているので今回はUserモデルへの追記だけで大丈夫です?
app/models/user.rb
class User < ApplicationRecord
has_many :posts
end
ルーティングの設定
基本的なCRUD処理のためのルーティングを設定していきます。
config/routes.rb
Rails.application.routes.draw do
# api, v1のような名前空間を定義するのがAPIの慣習?みたいなものです
namespace 'api' do
namespace 'v1' do
resources :users
resources :posts
end
end
end
定義されたルーティングを確認してみましょう。ターミナルでrails routes
、もしくは
rails s
してからブラウザにhttp://localhost:3000/rails/info/routesと入力すると確認できます。
GET /api/v1/users(.:format) api/v1/users#index POST /api/v1/users(.:format) api/v1/users#create GET /api/v1/users/:id(.:format) api/v1/users#show PATCH /api/v1/users/:id(.:format) api/v1/users#update PUT /api/v1/users/:id(.:format) api/v1/users#update DELETE /api/v1/users/:id(.:format) api/v1/users#destroy
GET /api/v1/posts(.:format) api/v1/posts#index
POST /api/v1/posts(.:format) api/v1/posts#create
GET /api/v1/posts/:id(.:format) api/v1/posts#show
PATCH /api/v1/posts/:id(.:format) api/v1/posts#update
PUT /api/v1/posts/:id(.:format) api/v1/posts#update
DELETE /api/v1/posts/:id(.:format) api/v1/posts#destroy
注目していただきたいのはnew, editが生成されていないことです。APIモードではRails側では描画を行わず、viewに必要なアクションを用意する必要がないため、自動生成もされていません。
Controllerの作成
最後にControllerを作成していきます。ルーティングでapi/v1
のような名前空間を定義したのでControllerでもそれに従います。
rails g controller api/v1/users index create show update destroy
rails g controller api/v1/posts index create show update destroy
app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < ApplicationController
before_action :set_user, except:[:index, :create]
def index
users = User.all
render status: 200, json: { data: users }
end
def create
user = User.new(user_params)
if user.save
render status: 200, json: { data: user }
else
render status: 400, json: { data: user.errors }
end
end
def show
render status: 200, json: { data: @user }
end
def update
if @user.update(user_params)
render status: 200, json: { data: @user }
else
render status: 400, json: { data: @user.errors }
end
end
def destroy
@user.destroy
render status: 200,json: { msg: 'ユーザーを削除しました。' }
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:name)
end
end
app/controllers/api/v1/posts_controller.rb
class Api::V1::PostsController < ApplicationController
before_action :set_post, except:[:index, :create]
def index
posts = Post.all
render status: 200, json: { data: posts }
end
def create
user = User.find(params[:user_id])
post = user.posts.create(post_params)
if post
render status: 200, json: { data: post }
else
render status: 400, json: { data: post.errors }
end
end
def show
render status: 200, json: { data: @post }
end
def update
if @post.update(post_params)
render status: 200, json: { data: @post }
else
render status: 400, json: { data: @post.errors }
end
end
def destroy
@post.destroy
render status: 200,json: { msg: '投稿を削除しました。' }
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:text, :user_id)
end
end
実践!
さっそく完成したので実際にリクエストを送ってみましょう!まずはユーザーを作っていきます。
curlコマンド
curl -X POST -H "Content-Type: application/json" -d '{"name":"first_user"}' localhost:3000/api/v1/users
すると、200のステータスコードと共にUserのJSONが返ってきます。
{
"data":
{
"id":1,
"name":"first_user",
"created_at":"2020-06-29T05:42:44.480Z",
"updated_at":"2020-06-29T05:42:44.480Z"
}
}
次は最初に作ったユーザーに紐付けた投稿を作成してみましょう。
curlコマンド
curl -X POST -H "Content-Type: application/json" -d '{"text":"Hello, rooter!", "user_id":1}' localhost:3000/api/v1/posts
{
"data":
{
"id":1,
"text":"Hello, rooter!",
"user_id":1,
"created_at":"2020-06-29T05:50:13.066Z",
"updated_at":"2020-06-29T05:50:13.066Z"
}
}
ユーザーに紐づいた投稿がしっかり作成されていますね!しかし、少し不満点があります。
せっかくユーザーに関連づけた投稿を作成したのに、ユーザーのGETエンドポイントを叩いてもそのユーザーの投稿の情報は返ってきません。
他にもupdated_atカラムは返したくないなど、実際の開発では色々と細かくJSONを整形したい要望が出てくるでしょう。そこで活躍するのがactive_model_serializersです!
active_model_serializersを使ったJSONの整形
どんなgem?
active_model_serializersはJSONを簡単に整形してくれるgemです。他にも同じようなgemや機能(jbuilderやto_json)はありますが、このgemには以下のメリットがあります。- レスポンスが速い
- SerializerクラスにJSONを整形する処理をまとめられる
百聞は一見に如かず、さっそく使ってみましょう!
導入
まずはgemをインストールします。現時点での最新版は0.10.0です。
Gemfile
gem 'active_model_serializers', '~> 0.10.0'
bundle install
次にJSONの整形に使うSerializerファイルを生成します。
rails g serializer user
自動生成されたserializerファイルにいろいろな記述を追加することで自由にJSONを整形することができます。ひとまず以下の3つをやってみます。
JSONに含める値を追加、削除したい
自動生成した直後ではファイルの状態は以下のようになっていると思います。
app/serializers/user_serializer
class UserSerializer < ActiveModel::Serializer
attributes :id
end
現在はattributesの部分にidしか指定されていません。ここに表示したいモデルのカラム名を入れることでJSONに含めることができます。今回はupdated_atは含めたくない、という想定にします。
class UserSerializer < ActiveModel::Serializer
# name, created_atを追加、含めたくないupdated_atは書かない
attributes :id, :name, :created_at
end
そしてcontrollerファイルにrender時にserializerで整形するというオプションを付けます。
app/controllers/api/v1/users_controller.rb
# とりあえずshowアクションだけ
def show
render status: 200, json: @user, serializer: UserSerializer
end
こうしてリクエストを送るとupdated_atを除いたJSONが返ってきます。
{
"id": 2,
"name": "aaa",
"created_at": "2020-06-29T05:35:35.994Z"
}
updated_atが表示されなくなりましたね、簡単!
今度は逆に新しい値を追加してみます。ユーザーのJSONに現在時刻を表すキーを追加してみます。
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :created_at, :current_time
def current_time
Time.now
end
end
{
"id":1,
"name":"first_user",
"created_at":"2020-06-29T05:49:59.659Z",
"current_time":"2020-06-29T14:54:55.637+09:00"
}
現在時刻を表すカラムを追加することができました!このようにactive_model_serializersを使えばJSONの追加、削除が簡単にできます。
ネストされたリソースを含めて表示したい
次はユーザーに関連付けた投稿の情報をユーザーのJSONに含めます。まずはgemの設定用のファイルをconfig/initializers/配下に作ります。
config/initializers/ams.rb
# 関連モデルもJSONにincludeする設定
ActiveModelSerializers.config.default_includes = '**'
次に serializerファイルにpostsを含める記述をします。
class UserSerializer < ActiveModel::Serializer
# posts を追加
attributes :id, :name, :posts, :created_at
end
追加したらサーバを再起動してからJSONを確認してみましょう。
{
"id":1,
"name":"first_user",
"posts":[
{
"id":1,
"text":"Hello, rooter!",
"user_id":1,
"created_at":"2020-06-29T05:50:13.066Z",
"updated_at":"2020-06-29T05:50:13.066Z"
}
],
"created_at":"2020-06-29T05:49:59.659Z"
}
postsが追加されました!こちらも簡単ですね!
JSONの構造を変えたい
実際のAPIサーバーを構築するときはJSONを何らかの形に沿って規則化した状態で返すほうが望ましいです。そこでactive_model_serializersでは設定のadapterをいじることで簡単に規則に沿うようにJSONを整形してくれます。
config/initializers/ams.rb
ActiveModelSerializers.config.default_includes = '**'
# 下の行を追加
ActiveModelSerializers.config.adapter = :json_api
サーバを再起動して出力を確認してみます。
{
"data":{
"id":"1",
"type":"users",
"attributes":{
"name":"first_user",
"posts":[
{
"id":1,
"text":"Hello, rooter!",
"user_id":1,
"created_at":"2020-06-29T05:50:13.066Z",
"updated_at":"2020-06-29T05:50:13.066Z"
}
],
"created-at":"2020-06-29T05:49:59.659Z"
}
}
}
新たにtype, attributesなどのキーが増えてより構造が明確になりました!設定一つでJSONを規則化することができる非常に強力な機能です。
まとめ
RailsのAPIモードとactive_model_serializersの入門を同時に詰め込んだ欲張りな構成でお送りしました。 実際の開発ではもっと細かくJSONを調整したいという場合も出てくると思いますが、ここで紹介した以外にもactive_model_serializersには様々なオプションや設定があります。
これを機にRailsのAPIモードやactive_model_serializersを使ってみたくなった!という方が一人でも増えたら嬉しいです。
CONTACT
お問い合わせ・ご依頼はこちらから