Edited at
LivesenseDay 16

RailsでAPI開発する前に知っておくべき4つのこと

More than 3 years have passed since last update.

Livesense Advent Calendar 2015 16日目の記事です。


はじめに

私は株式会社リブセンスで働く新卒1年目のエンジニアです。

入社後の半年ほど、弊社転職サービスのAndroidアプリのためのWeb APIを開発していました。

この記事では、半年間の開発で学んだ 少ないコード量で素早くWeb APIを開発するためのTips を紹介します。

ここで紹介する内容はそれぞれ、詳しく述べられているWebページが既に存在します。

よって、この記事は「読んでくださった方がAPI実装の流れをトレースできること」を目的とします。


想定読者


  • Railsを使ってJSON形式のWeb APIを開発したい方


    • Rails自体初めて触る人へのサポートはしていないので、まずはRailsの基礎を抑えてから読んでください




1. rails ではなく rails-api gemを使おう

RailsでAPIを作成するときは、ビューに関する機能やRack Middlewareを削ぎ落とした Rails APIを使いましょう。

(なお、rails-apiはRails5からRails本体にマージされます。)

1.1. Railsアプリケーションを作成


bash

$ gem install rails-api

$ rails-api new api_app

1.2. モデルを作成する


bash

$ bin/rails g scaffold user name:string mail:string password:string

invoke active_record
create db/migrate/20151214145437_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
invoke api_resource_route
route resources :users, except: [:new, :edit]
invoke scaffold_controller
create app/controllers/users_controller.rb
invoke test_unit
create test/controllers/users_controller_test.rb

1.3. 任意のdatabaseを設定してmigrate


2. ドキュメントは自動生成し、コードと一緒に管理しよう

JSON形式のWeb APIでは、リソースの型(JSON Schema)をまずはじめに定義しましょう。

APIをコールするクライアントは、リソースがドキュメントに定義された形式で返却されることを期待しています。

よって、ドキュメントと実装を常に一致させておくことは非常に重要です。

そのために、JSON-Hyper-Schemaを設計の中心にアプリケーションを作成します。

JSON-Hyper-SchemaはJSON-Schemaをハイパーメディア(HTTP上のリソース)として扱えるようにしたものです。

詳しくは以下の説明を参照してください。

JSON-Hyper-Schemaは人の手で作成・管理しようとすると大変なので、 prmd というgemを利用します。

prmdを使うことで、SchemaからAPIドキュメントを生成できます。

また、yamlで記述してJSONに変換することで、より管理がしやすくなります。

更に詳しいprmdの説明については以下の説明を参照してください。

JSON Schemaを上手く運用出来そうなprmdとその周りのお話

実際にprmdを使ってJSON-Hyper-Schemaとドキュメントを生成してみましょう。

2.1. prmdをbundle install

https://github.com/interagent/prmd

2.2. yaml形式でSchemaの雛形を生成


bash

$ mkdir -p schemata/yml

$ bundle exec prmd init --yaml user > schemata/yml/user.yml

2.3. yamlでSchemaを記述


schemata/yml/user.yml

---

"$schema": http://json-schema.org/draft-04/hyper-schema
title: User
description: User API
stability: prototype
strictProperties: true
type:
- object
definitions:
id:
description: unique identifier of user
example: 1
readOnly: true
type:
- integer
name:
description: user Name
example: 'otyoppu'
type:
- string
mail:
description: unique user email
example: 'otyoppu@example.com'
type:
- string
password:
description: user password
example: 'hogefugapiyo'
type:
- string
identity:
$ref: "/schemata/user#/definitions/id"
links:
- description: Info for existing user.
href: "/users/{(%2Fschemata%2Fuser%23%2Fdefinitions%2Fidentity)}"
method: GET
rel: self
title: Info
properties:
id:
"$ref": "/schemata/user#/definitions/id"
name:
"$ref": "/schemata/user#/definitions/name"
mail:
"$ref": "/schemata/user#/definitions/mail"
id: schemata/user

上記の例では、Userというリソースの定義と、受け付けるHTTPリクエスト(GET)とレスポンスを定義しています。

2.4. JSONファイルを出力


bash

$ mkdir -p schemata/json

$ bundle exec prmd combine schemata/yml/user.yml > schemata/json/user.json

2.5. ドキュメントを出力


bash

$ mkdir doc

$ bundle exec prmd doc schemata/json/user.json > doc/user.md

これでドキュメントの作成は完了です。

Githubなどでリポジトリ管理をしているのであれば、マークダウンのドキュメントをブラウザ上で閲覧できます。

実際の業務では、ここまでの一連の流れをRails::Generatorsを使うことでコマンド一発で実行しています。


3. ActiveModel::Serializerを使ってレスポンスを整形しよう

APIの実装においては、ビューの役割をSerializerが担うことで簡潔に記述ができます。

ActiveModel::Serializerを利用するには、active_model_serializersgemをインストールしてください。

ActiveModel::Serializerを使った実装例は以下の通りです。


app/controllers/users_controller.rb

class UsersController < ApplicationController

def show
render json: UserSerializer.new(user)
end

def user
@user ||= User.find(params[:id])
end
end



app/serializers/user_serializer.rb

class UserSerializer < ActiveModel::Serializer

# 日付データはレスポンスに含めたくないので、ここで出力するアトリビュートを制限します
attributes :id, :name, :mail, :password

# ビューを操作したい場合は、このように記述ができます。
# ここでは、パスワードをレスポンスに含めたくないので代わりの文字列を出力させています。
def password
'xxxxxxxxxx'
end
end



config/initializers/active_model_serializers.rb

# これを設定しないと、root(ここではUser)でJSONがネストされてしまいます

ActiveSupport.on_load(:active_model_serializers) do
ActiveModel::Serializer.root = false
ActiveModel::ArraySerializer.root = false
end


4. JSON-Hyper-Schemaを使ってテストしよう

APIドキュメントの生成で用いたJSON-Hyper-Schemaをここでも使います。

rack-json_schema というgemをインストールしてください。

Rack層で以下のことが可能になります。


  • JSON-Hyper-Schemaで定義した「型」と実際のレスポンスに乖離があった場合、即座にエラーを返す


    • テストを通したければ実装とドキュメントを一致させなくてはならなくなる



  • リクエストパラメータをJSON-Hyper-Schemaを使ってバリデーションできる


    • バリデーションをモデル内で実装しなくても、Rack層で自動的に行ってくれる



  • テスト内のAPIリクエストをJSON-Hyper-Schemaを使ってstubできる

より詳しい内容については以下を参照してください。

ここでは、Rspecを使って Rack::JsonSchema::ResponseValidation を試します。

4.1 config/application.rb でRack Middlewareを積む

schemaファイルは一つに統合しておく必要があるので、テストする度にcombineするようにしました。

(combineにはほとんど時間がかかりません)


config/application.rb

require File.expand_path('../boot', __FILE__)

require 'rails/all'

Bundler.require(*Rails.groups)

module ApiApp
class Application < Rails::Application
config.time_zone = 'Tokyo'
config.active_record.default_timezone = :local
config.autoload_paths += %W(#{config.root}/lib)

if ENV["RAILS_ENV"] == 'test'
# Rebuild ./schemata/schema.json
system('bundle exec prmd combine schemata/yml/* > schemata/schema.json')

schema = JSON.parse(File.read("#{Rails.root}/schemata/schema.json"))
config.middleware.use Rack::JsonSchema::ErrorHandler
config.middleware.use Rack::JsonSchema::ResponseValidation, schema: schema
end
end
end


これで、Schemaで定義したレスポンスと乖離があった場合テストが通らなくなります。

4.2 テストを実装する

以下のようにテストを実装してください。


spec/request/users_spec.rb

require 'rails_helper'

RSpec.describe 'Users', type: :request do
describe 'GET users/1' do
# 簡単のためこのようにしていますが、データ投入にはFixturesやFactoryGirlを使ってください
User.create(name: 'otyoppu', mail: 'otyoppu@example.com')

it 'ユーザの情報が取得できる' do
get '/users/1'

expect(response).to have_http_status(:ok)
expect(response.body).to be_json_as(
id: 1,
name: 'otyoppu',
mail: 'otyoppu@example.com',
password: 'xxxxxxxxxx'
)
end
end
end


テストを実行すると、以下のような結果が返ります。


bash

$ bundle exec rspec spec/request/users_spec.rb

{
"id": "invalid_response_type",
"message": "#: failed schema #/definitions/user: \"password\" is not a permitted key."
}
F

Failures:

1) Users GET users/1 ユーザの情報が取得できる
Failure/Error: expect(response).to have_http_status(:ok)
expected the response to have status code :ok (200) but it was :internal_server_error (500)
# ./spec/request/users_spec.rb:11:in `block (3 levels) in <top (required)>'

Finished in 0.0454 seconds (files took 3.63 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/request/users_spec.rb:7 # Users GET users/1 ユーザの情報が取得できる


HTTPステータスコードとレスポンスが Rack::JsonSchema::ResponseValidation によって書き換えられているのがわかります。

4.3 スキーマを修正する

Schemaを確認すると、レスポンスにpasswordが含まれていないことが分かったので、以下のように修正します。


schemata/yml/user.yml

---

"$schema": http://json-schema.org/draft-04/hyper-schema
title: User
description: User API
stability: prototype
strictProperties: true
type:
- object
definitions:
id:
description: unique identifier of user
example: 1
readOnly: true
type:
- integer
name:
description: user Name
example: 'otyoppu'
type:
- string
mail:
description: unique user email
example: 'otyoppu@example.com'
type:
- string
password:
description: user password
example: 'hogefugapiyo'
type:
- string
identity:
$ref: "/schemata/user#/definitions/id"
links:
- description: Info for existing user.
href: "/users/{(%2Fschemata%2Fuser%23%2Fdefinitions%2Fidentity)}"
method: GET
rel: self
title: Info
properties:
id:
"$ref": "/schemata/user#/definitions/id"
name:
"$ref": "/schemata/user#/definitions/name"
mail:
"$ref": "/schemata/user#/definitions/mail"
password:
"$ref": "/schemata/user#/definitions/password"
id: schemata/user

5.4 再度JSON-Hyper-Schemaとドキュメントを生成


bash

$ bundle exec prmd combine -o schemata/json/user.json schemata/yml/user.yml

$ bundle exec prmd doc schemata/json/user.json > doc/user.md

5.5 テストが通ることを確認


bash

$ bundle exec rspec spec/request/users_spec.rb

{"id":1,"name":"otyoppu","mail":"otyoppu@example.com","password":"xxxxxxxxxx"}
.

Finished in 0.03871 seconds (files took 3.64 seconds to load)
1 example, 0 failures



おわりに

この記事では、APIを開発する流れを紹介しました。

開発の流れを追う中でAPI開発に役立ついくつかの要素を学ぶことができたかと思います。

API開発では、JSON-Hyper-Schemaによる動的定義によってコード量を大幅に減らすことが可能です。

業務ではControllerの記述を減らすためのDSL実装などもしており、更に高速なAPI開発を可能にしています。

自前で実装しなくても、 Garageというgemを使って認証系を備えたAPIを簡単に実装することもできます。

https://github.com/cookpad/garage

紹介した技術を使って、よりよいWeb APIを作ってみてください!