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

  • 637
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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を作ってみてください!