Rails5 apiモード + JSONAPI ResourcesでAPIサーバを作る
jsonapi-resourcesはこちら
cerebris/jsonapi-resources: A resource-focused Rails library for developing JSON API compliant servers.
下準備
インストールまで
いつものなのでサクサクいきます。
$ bundle init
# Gemfile source 'https://rubygems.org' gem 'rails', '5.0.0.rc1'
$ bundle install $ bundle exec rails new . --api # 上書きを確認されるので適当にYesしておく
# Gemfile # 以下を追記。 # rubygemsにあるのだとRails5に対応していないのでgithubから取得していることに注意。 gem 'jsonapi-resources', git: '[email protected]:cerebris/jsonapi-resources.git', ref: '5e4fd81e516fe73395d8607520e96d6a013ff741'
設定変更
Controllerに機能追加(必須)
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base include JSONAPI::ActsAsResourceController end
開発しやすいようにconfigいじっておく(任意)
# config/environments/development.rb Rails.application.configure do # 以下2つを変更。 config.eager_load = true config.consider_all_requests_local = false end
開発しやすいようにinitializersとしてちょっといじっておく(こっちも任意)
#config/initializers/jsonapi-resources.rb # 各設定の意味はこちらを参照: https://github.com/cerebris/jsonapi-resources/blob/master/lib/jsonapi/configuration.rb JSONAPI.configure do |config| # :underscored_key, :camelized_key, :dasherized_key, or custom # jsで扱い易いようにJSONのkeyをlowerCamelCaseにする。 config.json_key_format = :camelized_key # :none, :offset, :paged, or a custom paginator name # pagenationの形式をoffsetに変更 config.default_paginator = :offset end
実装
サンプルはtwitter風な何かを考えましょう。
Userがいて各UserはTweetを複数もちます。
Userはpasswordを直接DBにもちます(サンプルなのに真面目にやるのが面倒なので)
$ bin/rails g model User screen_name:string name:string password:string $ bin/rails g model Tweet user_id:integer text:text
associationも貼りましょう。
# app/models/User.rb class User < ApplicationRecord has_many :tweets end
# app/models/Tweet.rb class Tweet < ApplicationRecord belongs_to :user end
はい、ここからがJSONAPI::Resourcesを使う本番です。
app/resources配下にAPIを書いていくことになります。
attributes
にAPIとして公開したい情報を書きます。
ここに記載されないものはAPIからは公開されません。
しかしassociationのものをここに書いてはいけません。 has_many
や has_one
を使って記述します
(※ これらは違う書き方もできます。詳しくは公式のREADMEを参照。)
# app/resources/user_resource.rb # class名はmodel名 + Resource class UserResource < JSONAPI::Resource # attributesでmodelにdispatchしたいメソッドを書く attributes :name, :screen_name # associationも書く has_many :tweets end
# app/resources/tweet_resource.rb class TweetResource < JSONAPI::Resource attributes :text end
これでAPIの定義はできたのでroutingとcontrollerを書いておきましょう。
actionは勝手に定義してくれるのでなんもなくてOKです。
$ bin/rails g controller users $ bin/rails g controller tweets
Rails.application.routes.draw do jsonapi_resources :users jsonapi_resources :tweets end
さて、では動作確認をしましょう。
$ bundle exec rake db:migrate $ rake routes Prefix Verb URI Pattern Controller#Action user_relationships_tweets GET /users/:user_id/relationships/tweets(.:format) users#show_relationship {:relationship=>"tweets"} POST /users/:user_id/relationships/tweets(.:format) users#create_relationship {:relationship=>"tweets"} PUT|PATCH /users/:user_id/relationships/tweets(.:format) users#update_relationship {:relationship=>"tweets"} DELETE /users/:user_id/relationships/tweets(.:format) users#destroy_relationship {:relationship=>"tweets"} user_tweets GET /users/:user_id/tweets(.:format) tweets#get_related_resources {:relationship=>"tweets", :source=>"users"} users GET /users(.:format) users#index POST /users(.:format) users#create user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy tweets GET /tweets(.:format) tweets#index POST /tweets(.:format) tweets#create tweet GET /tweets/:id(.:format) tweets#show PATCH /tweets/:id(.:format) tweets#update PUT /tweets/:id(.:format) tweets#update DELETE /tweets/:id(.:format) tweets#destroy $ bin/rails s
意気揚々とchromeとかでアクセスするとエラーが出ます。
{"errors":[{"title":"Not acceptable","detail":"All requests must use the 'application/vnd.api+json' Accept without media type parameters. This request specified 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'.","id":null,"href":null,"code":"406","source":null,"links":null,"status":"406","meta":null}]}
ちゃんとheaderを設定してcurlしてあげましょう。
$ curl -i -H "Accept: application/vnd.api+json" http://localhost:3000/users HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Content-Type: application/vnd.api+json; charset=utf-8 ETag: W/"8fe32e407a1038ee38753b70e5374b3a" Cache-Control: max-age=0, private, must-revalidate X-Request-Id: d80811e1-20b7-4259-8865-2ea1ad823899 X-Runtime: 0.012141 Transfer-Encoding: chunked {"data":[]}
適当にデータを突っ込みます。(seedでいいのかっていうのはおいといて)
# db/seed.rb if User.count == 0 3.times do |i| User.create(name: "name#{i}", screen_name: "screen_name#{i}", password: "pass#{i}") end end if Tweet.count == 0 User.all.each do |u| 10.times do |i| Tweet.create(user_id: u.id, text: "#{u.name} < text#{i}") end end end
$ bundle exec rake db:seed $ curl -H "Accept: application/vnd.api+json" http://localhost:3000/users | jq '.data | .[0]' { "id": "1", "type": "users", "links": { "self": "http://localhost:3000/users/1" }, "attributes": { "name": "name0", "screenName": "screen_name0" }, "relationships": { "tweets": { "links": { "self": "http://localhost:3000/users/1/relationships/tweets", "related": "http://localhost:3000/users/1/tweets" } } } }
attributesに指定していないのでpasswordが含まれていませんね。
またhas_manyでtweetsをもつとしたのでrelationshipsをもっています。
$ curl -H "Accept: application/vnd.api+json" http://localhost:3000/tweets | jq '.data | .[0]' { "id": "1", "type": "tweets", "links": { "self": "http://localhost:3000/tweets/1" }, "attributes": { "text": "name0 < text0" } }
tweetにはrelationshipsを指定していないのでattributesのみです。
注意点
belongs_toがない
has_oneを使いましょう。
APIリソースとして表現する以上、そのリソースを主体として考えるべきでbelongs_toなんてことはないんだ、という理由なのかと推測している。
attributeとしてassociationのもの指定してしまう
attributesでassociationを取れる名前を指定すればassociationの指定がなくてもとってくることは可能。
しかしN+1問題が起きてしまうためやるべきではないと思う
# app/resources/user_resource.rb class UserResource < JSONAPI::Resource attributes :name, :screen_name has_many :tweets end
$ curl -H "Accept: application/vnd.api+json" 'http://localhost:3000/users?include=tweets'
Started GET "/users?include=tweets" for ::1 at 2016-05-22 14:02:42 +0900 Processing by UsersController#index as API_JSON Parameters: {"include"=>"tweets"} User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 10], ["OFFSET", 0]] Tweet Load (0.4ms) SELECT "tweets".* FROM "tweets" WHERE "tweets"."user_id" IN (1, 2, 3) (0.2ms) SELECT COUNT(*) FROM "users" Completed 200 OK in 24ms (Views: 7.9ms | ActiveRecord: 0.8ms)
一方でattributesで指定したとき。
# app/resources/user_resource.rb class UserResource < JSONAPI::Resource attributes :name, :screen_name, :tweets end
$ curl -H "Accept: application/vnd.api+json" http://localhost:3000/users
Started GET "/users" for ::1 at 2016-05-22 14:04:04 +0900 Processing by UsersController#index as API_JSON User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 10], ["OFFSET", 0]] (0.2ms) SELECT COUNT(*) FROM "users" Tweet Load (0.3ms) SELECT "tweets".* FROM "tweets" WHERE "tweets"."user_id" = ? [["user_id", 1]] Tweet Load (0.3ms) SELECT "tweets".* FROM "tweets" WHERE "tweets"."user_id" = ? [["user_id", 2]] Tweet Load (0.2ms) SELECT "tweets".* FROM "tweets" WHERE "tweets"."user_id" = ? [["user_id", 3]] Completed 200 OK in 59ms (Views: 28.0ms | ActiveRecord: 2.4ms)