Ruby

Grape API入門:RubyのGrape gem 4.0でREST APIを作る手順とRails連携

Grape API入門:RubyのGrape gem 4.0でREST APIを作る手順とRails連携

Grapeは、RubyでREST風のJSON APIを作るためのフレームワークです。Rackアプリとして単体で動かすことも、Railsの routes.rb に mount して既存アプリのAPI層にすることもできます。この記事では、2026年9月に出たGrape 4.0.1を使い、インストールからRailsへのマウント、パラメータ検証、grape-entityによるレスポンス整形、バージョニング、エラー処理、RSpecでのテスト、grape-swaggerによるドキュメント出力までを、Ruby 4.0.6・Rails 8.1.4で実行したコードで説明します。3系以前から上げる人向けに、4.0で挙動が変わった箇所も実行結果つきでまとめました。

まとめ:Grape 4.0でREST APIを作るときの要点

  • 最新版と要件:Grapeの最新は4.0.1(2026-09-15公開)です。Ruby 3.3以上とactivesupport 7.2以上が必要です。
  • Railsへの組み込み:app/api にクラスを置いて routes.rb で mount します。モジュール名を API にするなら、Zeitwerk向けに inflect.acronym "API" の設定が必須です。設定しないと NameError で起動しません。
  • Grapeを選ぶ場面:返すものがJSONだけで、宣言的なパラメータ検証と複数バージョンの並走が要るAPIです。RailsでHTML画面やフォームも提供する予定なら、通常のRailsアプリを選ぶほうが追加設定を減らせます。
  • 4.0での注意:rescue_from でHashを返す書き方は500になります。ルートより下に書いた use・helpers・rescue_from はそのルートに効きません。上げる前に該当箇所を探して直してください。

以下、Grapeの位置付けと使い分けから順に説明します。

Grapeとは:Ruby製REST APIフレームワークの位置付け

公式READMEはGrapeを「a REST-like API framework for Ruby」と説明しています(ruby-grape/grape README v4.0.1)。RackやRails、Sinatraの上で動き、エンドポイントの定義、入力の検証と型変換、出力形式の切り替え、バージョニングをDSLで書けます。ビュー、ORM、アセットは持ちません。DBアクセスはRailsのActive Recordなど、外側の仕組みをそのまま使います。

最新版4.0.1と動作要件

RubyGemsに最初の版が登録されたのは2010年8月で、累計ダウンロードは8,700万回を超えます(RubyGems.org grape、2026-10-28時点)。メジャー版は2025年11月の3.0.0から1年足らずで4.0.0に進みました。

版 公開日 必要なRuby 主な変化
4.0.1 2026-09-15 3.3以上 ネストしたmountの再マウント不具合を修正
4.0.0 2026-09-07 3.3以上 3.2・3.3で予告した非推奨を削除
3.3.0 2026-06-20 3.3以上 Ruby 3.2を切り捨て
3.0.0 2025-11-15 3.0以上 Ruby 2.7を切り捨て・Rack 3.2対応

4.0.1の実行時依存は activesupport 7.2以上、rack 2.2.4以上、mustermann 4.0以上、dry-types、dry-configurable、zeitwerkです。Rails 7.1以前のアプリはactivesupportの条件を満たさないので、Rails側を先に上げる必要があります。GitHubのCIは Ruby 3.3・3.4・4.0 で回っています。手元のRubyが古い場合は、Rubyの最新バージョンと各系列のサポート期限で上げ先を決めてください。

Rails APIモード・Sinatraとの使い分け

Grapeが向くのは、返すものがJSONに限られ、パラメータの型と値域をエンドポイントの直前に宣言したい、v1とv2を並べて長く運用したい、という条件がそろうAPIです。version と params のDSLが、まさにこの2つのためにあります。

管理画面やフォームをRails自身で描画する予定なら、--api を付けない通常のRailsアプリで始めてください。別のフロントエンドへJSONだけを提供する構成なら、APIモードも選択肢です。通常のRailsアプリならビューやブラウザ向け機能を利用できます。APIモードからRailsで描画する画面を追加する場合は、コントローラの継承元やセッションなどの設定を見直す必要があります。手順はRails APIモードでの作成手順とJWT認証にまとめています。Sinatraは自由度が高い一方、入力検証もバージョニングも自前で書くことになります。チームで同じ規約のAPIを量産するなら、Grapeの決まった書き方のほうが揃えやすいです。

Grapeのインストールとconfig.ruでの単体起動

Railsを使わない場合は、Gemfileに grape とRackサーバーを入れて、config.ru にAPIクラスを書けば動きます。

# Gemfile
source "https://rubygems.org"

gem "grape", "~> 4.0"
gem "puma"
gem "rackup"
# config.ru
require "grape"

class PingAPI < Grape::API
  format :json

  get :ping do
    { message: "pong", grape: Grape::VERSION }
  end
end

run PingAPI

bundle install のあと bundle exec rackup -p 9393 で起動し、curl localhost:9393/ping を送ると {"message":"pong","grape":"4.0.1"} が返ります。format :json により、ブロックの戻り値のHashがJSONになり、content-type: application/json が付きます。定義していないパスへのリクエストは404と x-cascade: pass ヘッダーで返り、Railsなど外側のRackアプリに処理を譲れるようになっています。GrapeがRackの上でどう呼ばれるかはRackの仕様とミドルウェアの書き方で、Gemfileの書き方はgemとBundlerの使い方で説明しています。

RailsアプリにGrapeをマウントする手順

Railsでは、Grapeのクラスを app/api 配下に置き、config/routes.rb で mount します。ここではRails 8.1.4の rails new --api で作ったアプリに、ステータス投稿のAPIを載せます。モデルは bin/rails g model Status text:string visibility:string で生成し、bin/rails db:migrate でテーブルも作成済みとします。

app/apiの配置とroutes.rbのmount

# Gemfile(追記)
gem "grape", "~> 4.0"
gem "grape-entity", "~> 1.1"
app/api/api/base.rb                 # API::Base(入口)
app/api/api/v1/base.rb              # API::V1::Base(v1の共通設定)
app/api/api/v1/statuses.rb          # API::V1::Statuses(エンドポイント)
app/api/api/v1/entities/status.rb   # API::V1::Entities::Status(出力形式)
# app/api/api/base.rb
module API
  class Base < Grape::API
    prefix :api
    format :json

    mount API::V1::Base
  end
end
# config/routes.rb
Rails.application.routes.draw do
  mount API::Base => "/"
end

app 直下のディレクトリはRailsの自動読み込みの起点になるので、app/api/api/base.rb は API::Base として読まれます。api が2段になるのはこのためです。リソースごとにクラスを分け、mount で束ねると、URLの階層とファイルの配置が一致します。

Zeitwerkの「API」acronym設定

RailsのZeitwerkは、ファイル名 api を既定で Api と解釈します。このままでは API モジュールが見つからず、bin/rails zeitwerk:check は次のエラーで止まります。

NameError: uninitialized constant API::V1 (NameError)

    mount API::V1::Base
             ^^^^

READMEが指示するとおり、config/initializers/inflections.rb に頭字語として登録すると解決します。

# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym "API"
end

設定後に bin/rails zeitwerk:check を実行すると All is good! が出ます。2016年ごろの記事にある config.paths.add や autoload_paths を足す手順は、Zeitwerkが標準になった今のRailsでは不要です。この設定はアプリ全体に効くので、既存の app/models/api_client.rb は ApiClient ではなく APIClient を定義していると見なされます。既存クラスに Api で始まる名前があるなら、モジュール名を Api にしてacronymを設定しない選択もあります。

エンドポイントとパラメータ検証の書き方

v1の共通設定とエンドポイントを書きます。version、rescue_from、mount は、それを効かせたいルートより上に書いてください(4.0で厳密になった点で、後述します)。

# app/api/api/v1/base.rb
module API
  module V1
    class Base < Grape::API
      version "v1", using: :path

      rescue_from ActiveRecord::RecordNotFound do |e|
        error!({ error: "not_found", detail: e.message }, 404)
      end

      rescue_from Grape::Exceptions::ValidationErrors do |e|
        error!({ error: "invalid_params", detail: e.full_messages }, 400)
      end

      mount API::V1::Statuses
    end
  end
end
# app/api/api/v1/statuses.rb
module API
  module V1
    class Statuses < Grape::API
      resource :statuses do
        desc "ステータスの一覧を返す"
        params do
          optional :limit, type: Integer, default: 20, values: 1..100
        end
        get do
          statuses = ::Status.order(id: :desc).limit(params[:limit])
          present statuses, with: Entities::Status
        end

        desc "ステータスを1件作成する"
        params do
          requires :text, type: String, allow_blank: false, length: { max: 140 }
          optional :visibility, type: String, values: %w[public private], default: "public"
        end
        post do
          status = ::Status.create!(declared(params))
          present status, with: Entities::Status
        end

        route_param :id, type: Integer do
          desc "ステータスを1件返す"
          get do
            present ::Status.find(params[:id]), with: Entities::Status
          end

          desc "ステータスを削除する"
          delete do
            ::Status.find(params[:id]).destroy!
            body false
          end
        end
      end
    end
  end
end

resourceとroute_paramによるURL設計

resource :statuses がURLの階層を、route_param :id がパスの変数を作ります。prefix :api と version "v1", using: :path と合わせて、API::Base.routes は次の4本になります。

GET    /api/:version/statuses(.json)
POST   /api/:version/statuses(.json)
GET    /api/:version/statuses/:id(.json)
DELETE /api/:version/statuses/:id(.json)

route_param :id, type: Integer と型を付けると、/api/v1/statuses/abc のような数字でないIDはGrapeのルートに一致しません。リクエストはRails側に渡り、ほかのルートにも一致しなければ404になります。レスポンス形式はAcceptヘッダーやRailsの設定に依存します。JSONで400を返したいなら、型を外して params do requires :id, type: Integer end で検証してください。.json 拡張子付きのURLも同じエンドポイントに届きます。

paramsとdeclaredによる入力の検証

params do ... end に書いた条件を満たさないリクエストは、エンドポイントの本体に入る前に止まります。上のコードに対する実際のレスポンスは次のとおりです。

リクエスト ステータス レスポンスの要点
POST {"text":"hello"} 201 visibilityは既定値public
POST {"text":""} 400 text is empty
POST textが141文字 400 length less than or equal to 140
POST visibilityがfriends 400 does not have a valid value
GET ?limit=0 400 limit does not have a valid value
GET /statuses/999 404 not_found(rescue_from)
DELETE /statuses/1(id=1が存在する場合) 204 本文なし

POSTの既定ステータスは201、body false を返すと204です。declared(params) は params ブロックで宣言したキーだけを取り出すので、{"text":"x","admin":true} を送っても admin は create! に渡りません。Railsのコントローラでいう Strong Parameters の役割を、宣言そのものが兼ねています。rescue_from Grape::Exceptions::ValidationErrors を書かなかった場合、エラーは {"error":"text is missing"} のように、メッセージ1本の形で返ります。

grape-entityによるレスポンス整形

モデルをそのまま返すと、DBの列がすべてJSONに出ます。grape-entity(最新1.1.0、2026-06-04)のEntityクラスで、公開する項目と名前を決めます。

# app/api/api/v1/entities/status.rb
module API
  module V1
    module Entities
      class Status < Grape::Entity
        format_with(:iso8601) { |t| t.iso8601 }

        expose :id
        expose :text
        expose :visibility
        expose :created_at, as: :posted_at, format_with: :iso8601
      end
    end
  end
end

エンドポイントでは present モデル, with: Entities::Status と書きます。出力は {"id":1,"text":"hello","visibility":"public","posted_at":"2026-10-28T07:52:50Z"} で、updated_at は出ません。as: で列名と違うキー名を付けられるので、DBの列名を変えてもAPIの契約は変わりません。Entityなしでも、ブロックからHashを返せばJSONになります。項目数にかかわらず、公開項目を明示した単純な応答ならHashで足ります。複数のエンドポイントで出力定義を共有するならEntityを使うと重複を減らせます。

APIのバージョニングと複数バージョンの並走

version の指定方法は :path(既定)、:header、:accept_version_header、:param の4つです。

using: クライアントの指定方法 例
:path URLに版を含める /api/v1/statuses
:header Acceptのベンダー型 application/vnd.example-v1+json
:accept_version_header Accept-Versionヘッダー Accept-Version: v1
:param クエリ文字列 ?apiver=v1

迷ったら :path を選んでください。ログやブラウザで版がそのまま見え、CDNのキャッシュキーも分かれます。:header と :accept_version_header は、ヘッダーが無いリクエストを最初に一致した版で処理します。strict: true を付けるとヘッダー無しのリクエストを受け付けなくなり、既定では404、cascade: false も付けると406が返ります。

v2を足すときは、変えたいエンドポイントだけをv2に書き、v1に version "v1", "v2" と並べると、変えていないエンドポイントをv2でも共有できます。このとき、新しい版を先に mount する必要があります。

class V1 < Grape::API
  version "v1", "v2", using: :path
  format :json
  get(:statuses) { { from: "v1" } }
  get(:legacy) { { from: "v1" } }
end

class V2 < Grape::API
  version "v2", using: :path
  format :json
  get(:statuses) { { from: "v2" } }
end

class Root < Grape::API
  mount V2   # 新しい版を先に
  mount V1
end

この順なら /v2/statuses はv2の実装を、/v2/legacy はv1の実装を返します。mount V1 を先に書くと、/v2/statuses もv1の実装が返ります。エラーにはならないので、v2を足したのに挙動が変わらないときは、まずmountの順を確かめてください。

rescue_fromによるエラー応答の統一

例外は rescue_from で受け、error!(本文, ステータス) で返します。前述の API::V1::Base のように、例外クラスごとにステータスと本文の形を決めておくと、クライアントは error キーだけを見て分岐できます。rescue_from :all が包括的に捕捉するのは StandardError とそのサブクラスです。本番では内部のメッセージがそのまま外に出ないよう、本文を固定文言にしてログ側に詳細を残してください。

バリデーションエラーだけGrape既定の400のまま返したいときは、rescue_from :grape_exceptions を併記します。4.0からは、rescue_from StandardError のようにクラスで書いた包括ハンドラより :grape_exceptions が優先されるようになりました(UPGRADING.md)。3系では、この組み合わせでもバリデーションエラーが包括ハンドラ側の応答(500など)になっていました。

RSpecのrequest specによるGrape APIのテスト

Railsにマウントしているなら、rspec-railsのrequest specでURLを叩くのがいちばん手軽です。ルーティング、Grapeの検証、rescue_from、Entityの出力をまとめて確かめられます。

# Gemfile(test グループ)
group :test do
  gem "rspec-rails"
end
# spec/requests/api/v1/statuses_spec.rb
require "rails_helper"

RSpec.describe "API::V1::Statuses", type: :request do
  describe "POST /api/v1/statuses" do
    it "作成したステータスを201で返す" do
      post "/api/v1/statuses", params: { text: "hello" }

      expect(response).to have_http_status(201)
      expect(response.parsed_body).to include("text" => "hello", "visibility" => "public")
    end

    it "textが空なら400とエラー内容を返す" do
      post "/api/v1/statuses", params: { text: "" }

      expect(response).to have_http_status(400)
      expect(response.parsed_body["error"]).to eq("invalid_params")
    end
  end

  describe "GET /api/v1/statuses/:id" do
    it "存在しないidなら404を返す" do
      get "/api/v1/statuses/999"

      expect(response).to have_http_status(404)
      expect(response.parsed_body["error"]).to eq("not_found")
    end
  end
end

Gemfileへの追記後に bundle install を実行し、bin/rails g rspec:install で設定ファイルを生成したあと bundle exec rspec を実行すると 3 examples, 0 failures になります。request specの書き方はRails 8・RSpec 8でのrequest specの書き方で詳しく扱っています。Railsを使わない単体のGrapeなら、rack-test を入れて include Rack::Test::Methods し、def app = PingAPI を定義すれば同じように get "/ping" と書けます。

テストで Grape::Endpoint.before_each を使ってヘルパーを差し替えている場合、4.0からは spec_helper.rb に require "grape/testing" が必要です。3系では書かなくても読み込まれていましたが、4.0では NoMethodError になります。

grape-swaggerによるAPIドキュメントの出力

grape-swagger(最新2.2.0)は、desc と params の宣言からAPI仕様書のJSONを作ります。2.2.0はgrape 2.1以上5.0未満に対応しているので、4.0.1とそのまま組み合わせられます。

# Gemfile(追記)
gem "grape-swagger", "~> 2.2"
gem "grape-swagger-entity", "~> 0.7"

# app/api/api/base.rb の mount の下に追記
add_swagger_documentation(
  info: { title: "Status API" }
)

GET /api/swagger_doc で仕様書が返り、paths に /api/v1/statuses と /api/v1/statuses/{id} が並びます。出力の形式は "swagger": "2.0"、つまりOpenAPI 2.0です。OpenAPI 3系を前提にしたツールへ渡すなら、変換をはさんでください。表示するSwagger UIの立て方はSwagger Editor・UIをDockerで動かす手順を参照してください。

Grape 4.0へ上げるときの破壊的変更と移行手順

4.0.0は、3.2と3.3で非推奨にした書き方を削除した版です。CHANGELOGの大半は内部構造の整理ですが、アプリのコードに影響するものもあります。Grape 4.0.1で実行して挙動を確かめたものを中心に並べます。

変更 3系までの挙動 4.0の挙動
rescue_fromでHashを返す エラー応答として解釈 500 Invalid response
ルートより下のrescue_from・use 条件次第で効く そのルートには効かない
:grape_exceptionsと包括ハンドラ 包括ハンドラが優先 Grapeの例外は本来の400等
Grape::Endpoint.before_each 自動で読み込み require “grape/testing” が必要
http_digest 最初のリクエストで例外 クラス定義時にNoMethodError
descの default キー そのまま使えた default_response へ改名(非推奨化)
自作error_formatterのbacktrace 常に中身あり backtrace: true 以外は空配列

4.0のエラー処理・登録順の変更とreturnの非推奨

1つ目は rescue_from の戻り値です。3系で動いていた次の書き方は、4.0では503ではなく 500 {"error":"Invalid response"} を返します。error! に書き換えると、意図どおり503になります。

# 4.0では500になる
rescue_from :all do |e|
  { message: e.message, status: 503, headers: {} }
end

# 4.0での書き方
rescue_from :all do |e|
  error!({ error: e.message }, 503)
end

2つ目は登録の位置です。get の下に rescue_from ArgumentError を書いたAPIでは、例外がハンドラに捕まらずにそのまま外へ出ました。3系でこの配置が動いていたのは、同じ種類の登録が先に1つあった場合だけです。use、helpers、before も同じ扱いなので、すべてルートより上へ移してください。

3つ目はエンドポイント内の return です。3.0から非推奨で、next に置き換えます。4.0.1ではまだ値を返しますが、削除予告が出ている書き方なので、上げるついでに直しておくと次の版で困りません。

新機能では、HTTPの QUERYメソッド(RFC 10008)に対応しました。query "/search" do ... end と書くと、検索条件をリクエスト本文のJSONで受け取れます。GETのクエリ文字列に入りきらない複雑な検索条件を、安全で冪等なメソッドとして扱えます。

3系から上げる前の確認手順

Rubyを3.3以上、activesupportを7.2以上にしてから、次の文字列をリポジトリで検索します。rescue_from のブロック末尾のHash、http_digest、before_each、desc の default:、エンドポイント内の return です。加えて、各APIクラスでルート定義の後ろに use、helpers、rescue_from が書かれていないかを目で確認してください。ここは文字列検索では見つけにくく、しかも4.0ではエラーにならずに黙って効かなくなります。修正後にrequest specを流し、エラー時のステータスが変わっていないかを確かめます。変更の全体はCHANGELOG.mdとUPGRADING.mdの「Upgrading to >= 4.0.0」にあります。

Grapeで作ったAPIの改修や、既存システムとのAPI連携の設計を外部に頼みたい場合は、API開発・システム連携の支援でも相談を受け付けています。認証をRackミドルウェアとして足す方法は、Wardenのカスタム戦略とJWT認証が参考になります。

よくある質問

Grape gemとは何ですか?

RubyでREST風のJSON APIを作るためのフレームワークです。Rackアプリとして単体で動かすことも、Railsにmountして使うこともできます。エンドポイント、パラメータ検証、バージョニングをDSLで宣言できる一方、画面やORMは持ちません。

Grapeの最新バージョンと対応するRubyは?

2026年10月時点の最新は4.0.1(2026-09-15公開)で、Ruby 3.3以上とactivesupport 7.2以上が必要です。版の更新が速いので、導入時はRubyGems.orgのgrapeのページで最新を確認してください。

GrapeとRailsのAPIモードはどちらを使うべきですか?

JSONだけを返し、複数バージョンを並走させるAPIならGrapeが向きます。Rails自身で画面やフォームも提供する予定なら通常のRailsアプリを、別のフロントエンドへJSONだけを提供するならAPIモードを検討してください。

RailsにmountしたGrapeが「uninitialized constant API」で動かないのはなぜですか?

Zeitwerkが api を Api と解釈しているためです。config/initializers/inflections.rb に inflect.acronym "API" を追加すると解決します。bin/rails zeitwerk:check で確認できます。

grape-entityは必須ですか?

必須ではありません。Hashを返せばJSONになります。ただ、モデルをそのまま返すと全列が出るため、公開項目を管理したいAPIではEntityを使うほうが安全です。grape-swagger-entityを入れて desc の success: にEntityを渡すと、その項目定義が仕様書にも載ります。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.03 テックブログ ノジマ不正アクセスで約700万件漏えい:対象確認・窓口と乗っ取り注文を止める実装
  2. 2026.11.03 テックブログ クラブツーリズム不正アクセス:約432万件の対象確認と偽メールの見分け方・流出検知の設計
  3. 2026.10.29 テックブログ ソニー生命の情報漏洩38.5万件|ログインできない理由、再開状況と窓口の電話番号
  4. 2026.11.02 テックブログ アララの不正アクセス:メール約7,700万件の持ち出しと契約企業が行うAPIキー再設定・代替送信・連名報告
  5. 2026.11.03 テックブログ ネットマイル不正アクセスで最大549万人:対象確認と退会会員データを消す実装

RELATED POSTS 関連記事

目次