こんにちは、学生エンジニアの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を使ってみたくなった!という方が一人でも増えたら嬉しいです。

Pocket