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を渡すと、その項目定義が仕様書にも載ります。