Grape は RESTful な API を構築するためのマイクロフレームワークです。
今回は Grape を使って簡単な Web API を作っていきます。
[2015/03/07] ファイルの配置を変更しました。変更点は GitHub をご確認ください。
準備
$ rails g model MessageBoard title:string body:text
$ rails g model Comment message_board_id:integer body:text
$ rake db:migrate
$ rails g rspec:install
# app/models/message_board.rb class MessageBoard < ActiveRecord::Base has_many :comments end
API 配置用のディレクトリ作成
$ mkdir -p app/apis/api $ mkdir -p app/apis/entity
Grape gemのインストール
# Gemfile gem 'grape' gem 'grape-jbuilder'
$ bundle install
APIの実装
ディレクトリ構成とルーティングは以下のようになる予定です。
├── app │ ├── apis │ │ └── api │ │ ├── base.rb │ │ ├── v1 │ │ │ ├── base.rb │ │ │ ├── comments.rb │ │ │ └── message_boards.rb │ │ └── v2 │ │ ├── base.rb │ │ ├── comments.rb │ │ └── message_boards.rb │ └── views │ ├── apis │ │ └── api │ │ ├── v1 │ │ │ ├── comments │ │ │ │ ├── _comment.jbuilder │ │ │ │ └── index.jbuilder │ │ │ └── message_boards │ │ │ ├── _message_board.jbuilder │ │ │ ├── index.jbuilder │ │ │ └── show.jbuilder │ │ └── v2 │ │ ├── comments │ │ │ ├── _comment.jbuilder │ │ │ └── index.jbuilder │ │ └── message_boards │ │ ├── _message_board.jbuilder │ │ ├── index.jbuilder │ │ └── show.jbuilder
Verb | URI Pattern |
---|---|
GET | /api/v1/message_boards |
POST | /api/v1/message_boards |
GET | /api/v1/message_boards/:id |
PUT | /api/v1/message_boards/:id |
DELETE | /api/v1/message_boards/:id |
GET | /api/v1/message_boards/:message_board_id/comments |
POST | /api/v1/message_boards/:message_board_id/comments |
DELETE | /api/v1/message_boards/:message_board_id/comments/:id |
GET | /api/v2/message_boards |
POST | /api/v2/message_boards |
GET | /api/v2/message_boards/:id |
PUT | /api/v2/message_boards/:id |
DELETE | /api/v2/message_boards/:id |
GET | /api/v2/message_boards/:message_board_id/comments |
POST | /api/v2/message_boards/:message_board_id/comments |
DELETE | /api/v2/message_boards/:message_board_id/comments/:id |
APIのルーティング追加
# config/routes.rb Rails.application.routes.draw do mount API::Base => '/' end
app/apiをオートロードに追加する設定
# config/application.rb module GrapeExample class Application < Rails::Application config.paths.add File.join('app', 'apis'), glob: File.join('**', '*.rb') config.autoload_paths += Dir[Rails.root.join('app', 'apis', '*')] config.middleware.use(Rack::Config) do |env| env['api.tilt.root'] = Rails.root.join 'app', 'views', 'apis' end end end
ルーツ用のタスク
いつも通りrake routesしただけだとAPIで定義したルーティングが表示されないのでタスクを作成します。
# lib/tasks/routes.rake namespace :api do desc 'API Routes' task routes: :environment do API::Base.routes.each do |api| method = api.route_method.ljust(10) path = api.route_path.gsub(':version', api.route_version) puts " #{method} #{path}" end end end
「rake api:routes」でAPIのルーティングが確認できます。
Grape
# app/apis/api/base.rb module API class Base < Grape::API mount V1::Base mount V2::Base end end
# app/apis/api/v1/base.rb module API module V1 class Base < Grape::API format :json default_format :json # for Grape::Jbuilder formatter :json, Grape::Formatter::Jbuilder prefix :api # /apiというパスになる version 'v1', using: :path # /api/v1というパスになる # 例外ハンドル 404 rescue_from ActiveRecord::RecordNotFound do |e| rack_response({ message: e.message, status: 404 }.to_json, 404) end # 例外ハンドル 400 rescue_from Grape::Exceptions::ValidationErrors do |e| rack_response e.to_json, 400 end # 例外ハンドル 500 rescue_from :all do |e| if Rails.env.development? raise e else error_response(message: "Internal server error", status: 500) end end mount V1::MessageBoards mount V1::Comments end end end
# app/apis/api/v1/message_boards.rb module API module V1 class MessageBoards < Grape::API helpers do # Strong Parametersの設定 def message_board_params ActionController::Parameters.new(params).permit(:title, :body) end def set_message_board @message_board = MessageBoard.find(params[:id]) end # パラメータのチェック # パラメーターの必須、任意を指定することができる。 # use :attributesという形で使うことができる。 params :attributes do requires :title, type: String, desc: "MessageBoard title." optional :body, type: String, desc: "MessageBoard body." end # パラメータのチェック params :id do requires :id, type: Integer, desc: "MessageBoard id." end end resource :message_boards do desc 'GET /api/v1/message_boards' get '/', jbuilder: 'api/v1/message_boards/index' do @message_boards = MessageBoard.all end desc 'POST /api/v1/message_boards' params do use :attributes end post '/' do message_board = MessageBoard.new(message_board_params) message_board.save end desc 'GET /api/v1/message_boards/:id' params do use :id end get '/:id', jbuilder: 'api/v1/message_boards/show' do set_message_board end desc 'PUT /api/v1/message_boards/:id' params do use :id use :attributes end put '/:id' do set_message_board @message_board.update(message_board_params) end desc 'DELETE /api/v1/message_boards/:id' params do use :id end delete '/:id' do set_message_board @message_board.destroy end end end end end
# app/apis/api/v1/comments.rb module API module V1 class Comments < Grape::API helpers do # Strong Parametersの設定 def comment_params ActionController::Parameters.new(params).permit(:body) end def set_message_board @message_board = MessageBoard.find(params[:message_board_id]) end def set_comment @comment = @message_board.comments.find(params[:id]) end # パラメータのチェック params :attributes do requires :body, type: String, desc: "MessageBoard body." end # パラメータのチェック params :message_board_id do requires :message_board_id, type: Integer, desc: "MessageBoard id." end # パラメータのチェック params :id do requires :id, type: Integer, desc: "MessageBoard id." end end resource :message_boards do params do use :message_board_id end route_param :message_board_id do resource :comments do desc 'GET /api/v1/message_boards/:message_board_id/comments' get '/', jbuilder: 'api/v1/comments/index' do set_message_board @comments = @message_board.comments end desc 'POST /api/v1/message_boards/:message_board_id/comments' params do use :attributes end post do set_message_board @message_board.comments.create(comment_params) end desc "DELETE /api/v1/message_boards/:message_board_id/comments/:id" params do use :id end delete '/:id' do set_message_board set_comment @comment.destroy end end end end end end end
Jbuilder
今回はJbuilderを使ってJSONフォーマットを作っていきます。
# app/views/apis/api/v1/message_boards/index.jbuilder json.message_boards @message_boards do |message_board| json.partial! 'api/v1/message_boards/message_board', message_board: message_board json.comments message_board.comments do |comment| json.partial! 'api/v1/comments/comment', comment: comment end json.comment_count message_board.comments.count end
# app/views/apis/api/v1/message_boards/show.jbuilder json.message_board do json.partial! 'api/v1/message_boards/message_board', message_board: @message_board end
# app/views/apis/api/v1/message_boards/_message_board.jbuilder json.extract! message_board, :id, :title, :body, :updated_at
# app/views/apis/api/v1/comments/index.jbuilder json.comments @comments do |comment| json.partial! 'api/v1/comments/comment', comment: comment end
# app/views/apis/api/v1/comments/_comment.jbuilder json.extract! comment, :id, :body, :updated_at
RSpec
もちろんテストも書きました。
# spec/apis/api/v1/message_boards_spec.rb require 'rails_helper' RSpec.describe API::V1::MessageBoards, type: :request do describe 'GET /api/v1/message_boards' do it 'responds successfully' do FactoryGirl.create_list(:message_board, 2) get '/api/v1/message_boards' expect(response).to be_success expect(response.status).to eq(200) end end describe 'POST /api/v1/message_boards' do let(:path) { '/api/v1/message_boards' } let(:attributes) { FactoryGirl.attributes_for(:message_board) } describe 'validations' do context 'when title not exist in params' do let(:attributes) { FactoryGirl.attributes_for(:message_board).except(:title) } it do post path, attributes expect(response).not_to be_success expect(response.status).to eq(400) end end end it 'responds successfully' do post path, attributes expect(response).to be_success expect(response.status).to eq(201) end it 'creates a new MessageBoard' do expect { post path, attributes }.to change(MessageBoard, :count).by(1) end end describe 'GET /api/v1/message_boards/:id' do let(:message_board) { FactoryGirl.create(:message_board) } it 'responds successfully' do get "/api/v1/message_boards/#{message_board.id}" expect(response).to be_success expect(response.status).to eq(200) end end describe 'PUT /api/v1/message_boards/:id' do let(:message_board) { FactoryGirl.create(:message_board) } let(:attributes) { FactoryGirl.attributes_for(:message_board, title: changed_title) } let(:changed_title) { 'changed title' } let(:path) { "/api/v1/message_boards/#{message_board.id}" } describe 'validations' do context 'when title not exist in params' do let(:attributes) { FactoryGirl.attributes_for(:message_board).except(:title) } it do put path, attributes expect(response).not_to be_success expect(response.status).to eq(400) end end end it 'responds successfully' do put path, attributes expect(response).to be_success expect(response.status).to eq(200) end it 'updates MessageBoard' do put path, attributes expect(message_board.reload.title).to eq(changed_title) end end describe 'DELETE /api/v1/message_boards/:id' do let(:message_board) { FactoryGirl.create(:message_board) } it 'deletes a MessageBoard' do message_board expect { delete "/api/v1/message_boards/#{message_board.id}" }.to change(MessageBoard, :count).by(-1) end end end
# spec/apis/api/v1/comments_spec.rb require 'rails_helper' require 'rails_helper' RSpec.describe API::V1::Comments, type: :request do let(:message_board) { FactoryGirl.create(:message_board) } describe 'GET /api/v1/message_boards/:message_board_id/comments' do it 'responds successfully' do FactoryGirl.create_list(:comment, 2, message_board_id: message_board.id) get "/api/v1/message_boards/#{message_board.id}/comments" expect(response).to be_success expect(response.status).to eq(200) end end describe 'POST /api/v1/message_boards/:message_board_id/comments' do let(:path) { "/api/v1/message_boards/#{message_board.id}/comments" } let(:attributes) { FactoryGirl.attributes_for(:comment) } describe 'validations' do context 'when title not exist in params' do let(:attributes) { FactoryGirl.attributes_for(:comment).except(:body) } it do post path, attributes expect(response).not_to be_success expect(response.status).to eq(400) end end end it 'responds successfully' do post path, attributes expect(response).to be_success expect(response.status).to eq(201) end it 'creates a new Comment' do expect { post path, attributes }.to change(Comment, :count).by(1) end end describe 'DELETE /api/v1/message_boards/:message_board_id/comments/:id' do let(:comment) { FactoryGirl.create(:comment, message_board_id: message_board.id) } before { comment } it 'deletes a Comment' do expect { delete "/api/v1/message_boards/#{message_board.id}/comments/#{comment.id}" }.to change(Comment, :count).by(-1) end end end
今回作ったもの
関連する記事
JSONの生成に Jbuilder ではなく Grape::Entity を使ってみました。
こちらの方が個人的には好きです。
Grape::Entity の使い方 - kzy52's blog