Rails APIは、Ruby on Railsをビューを持たない「APIモード」で動かし、JSONを返すバックエンドに絞った構成です。ReactやVueのSPA、スマホアプリのバックエンドとして使われます。本記事は Rails 8.1.4(2026年9月24日リリース)・Ruby 4.0.6 で実際にアプリを作って動かした結果をもとに、作成手順・JSONの整形・認証・ReactとのCORS連携・Docker Composeの開発環境・API仕様書の生成を順に解説します。独学でReactとRails APIをつなぐと、CORS・認証・コンテナ間の接続の3か所で止まりがちです。その3か所は、実行時のエラー名まで示して先回りします。
まとめ:Rails APIの要点
- 作り方:
rails new アプリ名 --api。通常モードで28個あるミドルウェアが19個になり、Cookie・セッション・Flashが外れる。ApplicationControllerはActionController::APIを継承する。 - 最初のエンドポイント:
bin/rails g scaffoldでJSONを返すCRUDが生成される。成功201・検証エラー422・パラメータ欠落400・削除204が既定の応答。 - 認証:Rails 8の
bin/rails g authenticationはAPIモードだとCookie前提のまま500になる。Bearerトークンに書き換えるか、devise-jwt0.13.0 を使う。devise-jwtもrespond_to :jsonとActionController::MimeRespondsを足さないと動かない。 - React連携:別オリジンになるので
rack-cors(3.0.0)でフロントのオリジンを許可する。開発中だけならViteのproxyでCORS自体を避けられる。 - Docker:
rails newが作るDockerfileは本番用。開発用のDockerfileを別に書き、DB・API・フロントの3コンテナに分ける。
以下、作成手順から順に、コピーして動く形で示します。
Rails APIとは|APIモードと通常モードの違い
「Rails API」という言葉は2つの意味で使われます。1つはこの記事で扱うAPIモード、もう1つはRailsのクラスやメソッドを引くためのAPIリファレンス(api.rubyonrails.org)です。メソッドの引数を調べたいならリファレンス、JSONを返すバックエンドを作りたいならAPIモード、と読み分けてください。
APIモードはRails 5.0で標準機能になり、--api を付けて作成すると config/application.rb に config.api_only = true が入ります。ブラウザ向けの機能を最初から外し、JSONの入出力に要るものだけを残す設計です。
APIモードで外れるミドルウェア9個
Rails 8.1.4 で通常モードとAPIモードのアプリを1つずつ作り、bin/rails middleware の出力を比べると、通常モード28個に対しAPIモードは19個でした。外れたのは次の9個です。
| 外れるミドルウェア | 役割 |
|---|---|
| ActionDispatch::Cookies | Cookieの読み書き |
| ActionDispatch::Session::CookieStore | セッション |
| ActionDispatch::Flash | 画面遷移間のメッセージ |
| ActionDispatch::ContentSecurityPolicy::Middleware | CSPヘッダの付与 |
| Rack::MethodOverride | フォームの _method でPUT等を偽装 |
| Rack::TempfileReaper | アップロード一時ファイルの削除 |
| Propshaft::Server / Propshaft::QuietAssets | アセット配信(Gemごと入らない) |
| WebConsole::Middleware | エラー画面のコンソール(Gemごと入らない) |
残る19個には Rack::Head・Rack::ConditionalGet・Rack::ETag などHTTPキャッシュ系と、例外を整形する ActionDispatch::ShowExceptions が含まれます。ミドルウェアの仕組みそのものはRackとは?Rubyのサーバーとアプリをつなぐ仕様とミドルウェアの書き方【Rack 3.2】で解説しています。
ActionController::API に無いモジュール
コントローラ側も削られています。Rails 8.1.4 のソースでは、ActionController::API が読み込むモジュールは17個です。StrongParameters・Rescue・RateLimiting・ParamsWrapper などは入っていますが、Cookies・MimeResponds(respond_to)・ヘルパー(helper_method)・Flash は入っていません。Railsガイドの「Using Rails for API-only Applications」も、Cookies や MimeResponds は必要になったら自分で include する追加候補として挙げています。
この差は、後半の認証で必ず効いてきます。通常モード向けに書かれたコードやgemは cookies や respond_to を当然のように呼ぶため、APIモードに持ち込むと NameError や NoMethodError で止まります。
APIモードを選ぶ条件
判断は「Railsが画面を描くかどうか」で決まります。画面をReact・Vue・スマホアプリが描き、Railsはデータを返すだけならAPIモードです。管理画面だけはRailsで描きたい場合は、通常モードのまま respond_to でJSONを併用するほうが手戻りが少なく済みます。APIモードでHTML画面を後から足すと、外したミドルウェアとモジュールを1つずつ戻す作業になるためです。RailsにこだわらずRubyで軽量なAPIだけを作るなら、Grape(Ruby)でREST APIを構築する実装ガイドも選択肢に入ります。
Rails APIの作成手順|rails new –api から最初のエンドポイントまで
ここではブログ記事(Article)を扱うAPIを作ります。DBは開発用にSQLite(既定)、Docker構成ではPostgreSQLを使います。
rails new –api で生成されるもの
gem install rails -v 8.1.4
rails new blog_api --api
cd blog_api
生成された Gemfile では、API開発で使うgemの多くがコメントアウトされた状態で入っています。jbuilder(JSONテンプレート)、bcrypt(パスワードのハッシュ化)、rack-cors(CORS)の3つは、使うときに行頭の # を外します。通常モードでは jbuilder が最初から有効なので、ここは両モードで異なる点です。
CORSの設定ファイル config/initializers/cors.rb も全行コメントアウトで生成されます。もう1つ見落としやすいのが Dockerfile で、冒頭に「This Dockerfile is designed for production, not development.」と書かれた本番用です(生成元のテンプレート)。開発環境に流用できない理由はDockerの章で扱います。
scaffoldでCRUDを作りcurlで確かめる
APIモードで scaffold を実行すると、ビューを作らず、JSONを返すコントローラだけが生成されます。ジェネレータの基本はRailsジェネレータ(rails generate)の使い方にまとめています。
bin/rails g scaffold Article title:string body:text
bin/rails db:migrate
生成されたコントローラは、Rails 8から入った params.expect で受け取る値を絞り、検証エラーには :unprocessable_content(422)を返します。古い記事に出てくる :unprocessable_entity は Rack 3.1 で非推奨になった旧名です。
# app/controllers/articles_controller.rb(生成物の抜粋)
def create
@article = Article.new(article_params)
if @article.save
render json: @article, status: :created, location: @article
else
render json: @article.errors, status: :unprocessable_content
end
end
private
def article_params
params.expect(article: [ :title, :body ])
end
モデルに validates :title, presence: true を足してサーバーを起動し、curlで叩いた結果が次の表です。
curl -i -X POST localhost:3000/articles \
-H "Content-Type: application/json" \
-d '{"title":"hello","body":"x"}'
| リクエスト | 応答 |
|---|---|
| POST(title・body あり) | 201 Created |
| POST(title なし) | 422 + {“title”:[“can’t be blank”]} |
| POST(article も title も無いJSON) | 400 Bad Request |
| DELETE /articles/1 | 204 No Content |
| GET /articles/999 | 404 Not Found |
JSONを {"article":{...}} で包まず {"title":"hello"} と平たく送っても201になるのは、ParamsWrapper がコントローラ名から article キーを補うからです。React側は包まずに送ってかまいません。
api/v1 名前空間でバージョンを切る
フロントやアプリと長く付き合うAPIは、URLに版を入れておくと仕様変更時に旧版を残せます。ルーティングを名前空間で囲み、コントローラを api/v1 の下に作ります。
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :articles
end
end
end
bin/rails g scaffold_controller api/v1/article title:string body:text \
--model-name=Article --skip-routes
ここに罠が2つあります。属性(title:string body:text)を省くと、生成される article_params が params.fetch(:article, {}) になり、POSTが ActiveModel::ForbiddenAttributesError で500になります。属性を渡しても、create の location: @article が名前空間に無い article_url を探すため、レコードは保存されたのに応答だけ500になります。location: [:api, :v1, @article] に直すと201と location: http://localhost:3000/api/v1/articles/3 が返りました。
既存アプリをAPIモードへ切り替える
既存アプリは config/application.rb に config.api_only = true を足し、ApplicationController の継承元を ActionController::API に変えます。ただし前述のとおりCookie・セッション・respond_to が消えるので、これらを使っているコードは全部動かなくなります。ビューが残っている途中のアプリなら、APIモードへ切り替えずに render json: を返すコントローラを追加するほうが安全です。
JSONの整形|as_json・jbuilder・Albaの選び方
属性が少ないうちは render json: と as_json(only: [...], include: [...]) で足ります。ネストや条件分岐が増えたら専用の仕組みに移ります。2026年10月時点の各gemの状況は次のとおりです。
| 手段 | 最新版 | 状況 |
|---|---|---|
| as_json / render json: | Rails標準 | 追加gem不要 |
| jbuilder | 2.15.1 | Rails公式。–api ではコメントアウト |
| Alba | 4.0.1 | 開発が活発。4.0.0で非推奨APIを削除 |
| active_model_serializers | 0.10.16 | 0.10系の保守のみ |
新規なら、まず as_json で始め、整形の規則がコントローラに散らばってきた時点で Alba に寄せるのが手戻りの少ない進め方です。Alba は 2026年8月28日の 4.0.0 で、非推奨だった推論API・Alba.resource_with・converter フックなどを削除しました(Albaのリリース一覧)。古い解説記事のコードを写すと 4.x では動かない箇所があるので、版を確認してから使ってください。
active_model_serializers は、READMEに「0.10系は0-10-stableブランチで保守を続けるが、それ以外の積極的な開発はしない」と書かれています(公式リポジトリ)。既存アプリでは依存関係と保守状況を確認して継続利用を判断します。新規開発では、既存資産の再利用などの要件がなければ、まずas_jsonかAlbaを検討します。
Rails API認証の実装|標準ジェネレータとdevise-jwt
「railsapi 認証」「react railsapi 認証」で探している人がつまずくのは、通常モード向けの認証をAPIモードに持ち込んだ時点です。Rails 8から標準の認証ジェネレータが入りましたが、これもそのままでは動きません。方式の選び方の全体像はAPI認証の設計|方式選定マトリクスとトークン寿命・監査ログの決め方で扱っているので、ここではRailsでの実装に絞ります。
bin/rails g authentication をBearerトークンで動かす
Rails 8.1.4 の認証ジェネレータには --api オプションがありますが、効果は「メール送信用以外のビューを作らない」だけです。生成されるAuthenticationモジュールはセッションIDを署名付きCookieに保存し、未ログイン時はログイン画面へリダイレクトします。APIモードのアプリで生成して叩くと、次の2つの例外で500が返りました。
NoMethodError (undefined method 'helper_method' for class ApplicationController)
NameError (undefined local variable or method 'cookies' for an instance of SessionsController)
どちらも前述の「ActionController::API に無いモジュール」が原因です。ただし、ユーザー・セッションのテーブル、has_secure_password、ログイン試行の回数制限(rate_limit)はそのまま使えます。Cookieの代わりにトークンをヘッダで受け取るよう、Sessionモデル・Authenticationモジュール・SessionsControllerの3ファイルを書き換えます。まずセッションにトークン列を足します。
bin/rails g migration AddTokenToSessions token:token
bin/rails db:migrate
# app/models/session.rb
class Session < ApplicationRecord
belongs_to :user
has_secure_token :token
end
# app/controllers/concerns/authentication.rb
module Authentication
extend ActiveSupport::Concern
include ActionController::HttpAuthentication::Token::ControllerMethods
included do
before_action :require_authentication
end
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
end
private
def require_authentication
resume_session || request_http_token_authentication
end
def resume_session
Current.session ||= authenticate_with_http_token do |token, _options|
Session.where(created_at: 30.minutes.ago..).find_by(token: token)
end
end
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session
end
end
def terminate_session
Current.session.destroy
end
end
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
allow_unauthenticated_access only: :create
rate_limit to: 10, within: 3.minutes, only: :create,
with: -> { render json: { error: "Try again later." }, status: :too_many_requests }
def create
if user = User.authenticate_by(params.permit(:email_address, :password))
session = start_new_session_for(user)
render json: { token: session.token }, status: :created
else
render json: { error: "Invalid email address or password." }, status: :unauthorized
end
end
def destroy
terminate_session
head :no_content
end
end
この構成で叩くと、トークン無しの GET /articles は401(www-authenticate: Token realm="Application")、POST /session でトークンが返り、Authorization: Bearer トークン を付けると200、DELETE /session の後は同じトークンが401になりました。authenticate_with_http_token は Token と Bearer のどちらの書き方も受け付けます。ヘッダの書式はBearer Token(ベアラートークン)とは?で確認できます。
トークンの実体はDBの行なので、ログアウトや強制失効がその場で効きます。上の resume_session は作成から30分を過ぎたセッションを検索から外すので、古いトークンも401になります。代わりにリクエストのたびにDBを1回引きます。API1本とフロント1本の構成なら、この方式で足ります。
devise-jwt でJWTを発行・失効させる
複数のサービスで署名付きトークンを検証する設計にはJWTを使えます。ただし、以下のdevise-jwt構成ではユーザー検索と失効リストの確認にDBアクセスが発生します。ここからは標準の認証ジェネレータを導入していない別のAPIアプリを前提に、devise と devise-jwt を組み合わせます。前節のUserモデルやAuthenticationモジュールに、そのまま追加する手順ではありません。2026年10月時点の最新は devise 5.0.4、devise-jwt 0.13.0(2026年1月13日公開・devise 4.0以上6.0未満に対応)です。JWTの構造と署名検証の仕組みはJWTとは?構造・署名検証の仕組みとセッション・OAuth/OIDCとの違いを実装視点で解説にまとめています。
# Gemfile
gem "devise"
gem "devise-jwt"
bundle install
bin/rails g devise:install
bin/rails g devise User
bin/rails g model JwtDenylist jti:string:index exp:datetime
bin/rails db:migrate
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
end
# app/models/jwt_denylist.rb
class JwtDenylist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Denylist
end
# config/initializers/devise.rb(Devise.setup の中に追記)
config.navigational_formats = []
config.jwt do |jwt|
jwt.secret = Rails.application.credentials.devise_jwt_secret_key!
jwt.expiration_time = 30.minutes.to_i
end
# config/routes.rb
devise_for :users, defaults: { format: :json }
ここまでで止めると動きません。APIモードのアプリで POST /users/sign_in を叩くと ActionController::UnknownFormat で406が返り、ApplicationController に respond_to :json を足すとログインは通るものの、今度は DELETE /users/sign_out が undefined method 'respond_to' で500になりました。500のあいだはトークンが失効せず、ログアウト後も同じトークンで200が返ります。devise-jwtの公式Wikiが示すとおり MimeResponds と respond_to :json を足し、Devise以外のコントローラに認証をかけて初めて一通り動きます。
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include ActionController::MimeResponds
respond_to :json
before_action :authenticate_user!, unless: :devise_controller?
end
署名鍵は secret_key_base と共用せず、bin/rails secret で生成した値を bin/rails credentials:edit で devise_jwt_secret_key として保存します。devise-jwtのREADMEも、ほかの部品と鍵を共有すると1か所の脆弱性の影響が広がるとして別の鍵を勧めています。
この状態でトークン無しで GET /articles を叩くと401、ログインすると201とともに authorization: Bearer eyJ... ヘッダが返り、ペイロードには sub(ユーザーID)・scp(スコープ user)・jti・iat・exp(30分後)が入っていました。ログアウトすると jwt_denylists に jti が1行記録され、同じトークンは以後401になります。なお、この構成ではサインアウトの応答自体も401でした(失効は成功しています)。React側でログアウト時の401をエラー扱いしないよう注意してください。DeviseがJWTを検証する内部のしくみはWardenのカスタム戦略の作り方で読めます。
自作BearerトークンとJWTの選択基準
JWTは「ステートレスだから失効できない」と言われますが、上のとおり devise-jwt でもログアウトを効かせるには失効リストのテーブルを引きます。DBでセッションを管理するなら、前節の自作Bearerトークン方式は追加gemを抑えられます。ただしRails標準の完成済み方式ではなく、有効期限や失効などの実装・保守を自分で担います。単一APIでセッションの実装・保守を担えるなら自作Bearerトークン、複数APIで署名検証を共有する要件があるなら失効方法も設計したうえでJWT、と決めてかまいません。
フロントでのトークン保存とセキュリティ
Reactでトークンを localStorage に置くと実装は簡単ですが、XSSが1か所でもあればスクリプトから読み出されます。HttpOnly のCookieに置けばスクリプトからは読めませんが、APIモードではCookieのミドルウェアを戻す必要があり、CSRF対策も要ります。どちらを選んでも、有効期限を短く(上の例では30分)し、XSS対策を先に固めるのが前提です。Cookie方式を選ぶ場合のCSRF対策はCSRFトークンとは?仕組み・Double Submit CookieとSPAでの実装を参照してください。
React × Rails API連携|CORS設定とデータ取得
Reactの開発サーバー(Viteの既定は5173番)とRails(3000番)はポートが違うので、ブラウザから見ると別オリジンです。Rails側が許可を返さないと、ブラウザはレスポンスをJavaScriptに渡しません。「react railsapi」で連携を試して最初に止まるのがここです。
rack-cors でCORSを許可する
Gemfile の rack-cors のコメントを外して bundle install し、cors.rb を書き換えます。2026年10月時点の最新は rack-cors 3.0.0(2025年5月16日公開)で、Rack 3.0.14以上が必須になりました(rack-corsのCHANGELOG)。Rails 8.1.4 の新規アプリはRack 3.2.7なので、そのまま入ります。
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "http://localhost:5173"
resource "*",
headers: :any,
expose: ["Authorization"],
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
insert_before 0 で全ミドルウェアの先頭に入るので、bin/rails middleware の1行目が use Rack::Cors になっていれば設定は効いています。expose の Authorization は、devise-jwtのようにトークンをレスポンスヘッダで返す場合に必須です。これが無いと、ヘッダは届いているのにJavaScriptから読めません。
curlでプリフライト(OPTIONS)を送って確かめた挙動は次のとおりです。
- 許可したオリジン(
http://localhost:5173):access-control-allow-originと、要求したヘッダを並べたaccess-control-allow-headersが返る。 - 許可していないオリジン:ステータスは200のまま、CORSヘッダだけが付かない。サーバーのログにエラーは出ないので、ブラウザのコンソールを見ないと原因に気づけない。
http://127.0.0.1:5173から開いた場合も、localhostとは別オリジンなのでヘッダが付かない。ブラウザで開くURLとoriginsの表記をそろえる。
本番でCookieを送らせるために credentials: true を付けたまま origins "*" にすると、rack-cors 3.0.0 は設定を読み込んだ時点で CorsMisconfigurationError(Allowing credentials for wildcard origins is insecure)で止まります。本番のオリジンは具体的なドメインで列挙してください。
Viteのproxyで開発中のCORSを避ける
開発中だけなら、Viteの server.proxy で /api へのリクエストをRailsへ転送すると、ブラウザからは同じオリジンに見えるのでCORS設定が要りません(Viteのserver.proxy)。前章でルートを /api/v1 に置いたので、パスの書き換えも不要です。
// front/vite.config.js
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: false,
},
},
},
})
ここでよく写される changeOrigin: true にすると、転送先のホスト名がHostヘッダに入ります。Docker Composeで転送先を http://api:3000 にしたところ、Railsの開発環境が許可するホスト(localhost やIPアドレス)に api が無いため、Blocked hosts: api:3000 の403が返りました。changeOrigin: false に戻すと200です。true のまま使うなら、Rails側の config/environments/development.rb に config.hosts << "api" を足します。proxyは開発サーバーだけの機能なので、本番でフロントとAPIを別ドメインに置くなら前節のCORS設定は結局必要です。
React側からのデータ取得とトークン付与
ログインで受け取ったトークンを、以降のリクエストの Authorization ヘッダに付けます。Create React Appは新規アプリ向けには非推奨となり、メンテナンスモードに移行しています。このReact SPAの作成には npm create vite@latest front -- --template react を使います。
// 記事一覧の取得(proxy経由なので相対パスで書ける)
const token = localStorage.getItem("token");
const res = await fetch("/api/v1/articles", {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (res.status === 401) {
localStorage.removeItem("token");
throw new Error("認証が必要です。再ログインしてください。");
}
if (!res.ok) {
throw new Error(`APIエラー: ${res.status}`);
}
const articles = await res.json();
proxyを使わずRailsへ直接送る構成では、URLを直書きせず import.meta.env.VITE_API_URL のような環境変数に逃がすと、開発と本番で切り替えられます。
DockerでReact+Rails API開発環境を構築する
「docker railsapi」「独学 react railsapi docker」で探す人の多くは、RubyとNodeとPostgreSQLを手元に入れずに一式を動かしたいはずです。ここでは Rails API(api/)とReact(front/)を1つのリポジトリに並べ、直下の compose.yaml でDB・API・フロントの3コンテナを起動します。Ruby 4.0.6・PostgreSQL 18・Node 24・Vite 8.3.4 の組み合わせで、3コンテナが起動してAPIの /up とフロントがどちらも200を返すところまで確認した構成です。
開発用のDockerfileを別に書く
rails new が作る Dockerfile は、RAILS_ENV="production" と BUNDLE_WITHOUT="development" を設定する本番用です。compose で build: ./api と書くとこのファイルが使われ、開発用のgemが入らない本番設定のコンテナになります。開発用は Dockerfile.dev として別に置きます。
# api/Dockerfile.dev
FROM ruby:4.0
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install
PostgreSQLを使うので、アプリは rails new api --api -d postgresql で作ります。接続先は環境変数で切り替えられるよう、config/database.yml の default に3行足します。
# api/config/database.yml(default に追記)
host: <%= ENV.fetch("DB_HOST", "localhost") %>
username: <%= ENV.fetch("DB_USERNAME", "postgres") %>
password: <%= ENV["DB_PASSWORD"] %>
コンテナの中から見た localhost はAPIコンテナ自身なので、DBにはサービス名の db で接続します。これを localhost のままにすると、APIコンテナ自身へ接続しようとするため、別コンテナのPostgreSQLには接続できません。
3コンテナ構成のcompose.yaml
# compose.yaml
services:
db:
image: postgres:18
environment:
POSTGRES_PASSWORD: password
volumes:
- pg_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 2s
retries: 15
api:
build:
context: ./api
dockerfile: Dockerfile.dev
command: bash -c "rm -f tmp/pids/server.pid && bin/rails db:prepare && bin/rails s -b 0.0.0.0"
environment:
DB_HOST: db
DB_PASSWORD: password
volumes:
- ./api:/app
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
front:
image: node:24
working_dir: /app
command: bash -c "npm install && npm run dev -- --host"
volumes:
- ./front:/app
- /app/node_modules
ports:
- "5173:5173"
depends_on:
- api
volumes:
pg_data:
古い記事の書き方から変えた点が4つあります。
- ボリュームのパス:公式の
postgresイメージは18以降、データの置き場所が/var/lib/postgresql/18/dockerになり、マウント先は/var/lib/postgresqlが推奨になりました(Docker Hubのpostgres公式イメージ)。17以前の/var/lib/postgresql/dataを18に流用しない。 - healthcheck:
depends_onだけでは「DBコンテナが起動した」時点でAPIが動き出し、受付前のPostgreSQLにdb:prepareが当たることがある。pg_isreadyが通るまで待たせる。 - server.pid の削除:コンテナを強制終了すると
tmp/pids/server.pidが残り、次の起動が「A server is already running」で止まる。 - node_modules の匿名ボリューム:macOSやWindowsの
node_modulesをLinuxのコンテナに持ち込まないよう、コンテナ側で別に持つ。
docker compose up で起動し、APIは localhost:3000、Reactは localhost:5173 で開けます。フロントからAPIへのproxyを使う場合は、vite.config.js の target を http://api:3000 にし、前章の changeOrigin の注意に従ってください。Composeの書式そのものはdocker-composeとは?複数コンテナをymlで定義し一括管理する仕組みを解説で、Dockerを使わずエディタのDev Containerで組む方法はruby on rails を vscode で開発する環境構築【2026年版】で解説しています。
API仕様書の自動生成|rswagでOpenAPIを出力・検証する
フロントと分業し、APIの変更が多い場合は、リクエストスペックと仕様書を連携させると更新漏れを減らせます。rswag(2026年10月時点で2.17.0・2025年11月5日公開)は、RSpecのリクエストスペックからOpenAPIの定義ファイルを生成し、Swagger UIで表示します。READMEが対象に挙げているのはOpenAPI 3.0とSwagger 2.0です(rswagの公式リポジトリ)。RSpecが前提なので、rspec-rails を先に入れておきます。
# Gemfile
gem "rswag"
group :development, :test do
gem "rspec-rails"
end
bundle install
bin/rails g rspec:install
bin/rails g rswag:install
# spec/requests/api/v1/articles_spec.rb
require "swagger_helper"
RSpec.describe "api/v1/articles", type: :request do
path "/api/v1/articles" do
get "記事一覧を取得" do
produces "application/json"
response "200", "articles found" do
run_test!
end
end
end
end
rake rswag:specs:swaggerize で定義ファイルが出力されます。定義生成は既定ではdry-runです。検証も実行するには RSWAG_DRY_RUN=0 bundle exec rake rswag:specs:swaggerize を使います。掲載例にはレスポンスのschemaがないため、本文の構造変更は検出できません。構造を検証する場合はschemaを定義し、認証済みAPIにはテスト用トークンも渡します。
Rails APIから外部サービスを呼ぶ|HTTPクライアントとWebhook
Rails APIは、決済・通知・SaaSのAPIを呼ぶ側にもなります。HTTPクライアントは標準の Net::HTTP でも書けますが、再試行やログをミドルウェアで差し込める faraday(2.15.0)が扱いやすい選択です。f.request :json と f.response :json を積めば、送信の直列化と受信のパースをFaraday側に任せられます。
外部サービスからのWebhookを受けるときは、受信したらすぐ200を返し、重い処理はジョブに回します。受信の処理中に外部APIを呼んで時間がかかると、送信元がタイムアウトして同じイベントを再送してきます。ジョブの実行基盤はSidekiqとは?Rails 8.1での導入・リトライ・Web UI設定が参考になります。
# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
allow_unauthenticated_access # Rails 8標準の認証を入れた場合
def receive
event = JSON.parse(request.raw_post)
ProcessWebhookJob.perform_later(event)
head :ok
end
end
Webhookのエンドポイントはログイン不要にする代わりに、送信元が付ける署名ヘッダを必ず検証してください。基幹システムや複数のSaaSをAPIでつなぐ設計から任せたい場合は、一創のAPI開発・システム連携でも支援しています。
よくある質問
Rails APIモードと通常モードはどちらを選ぶべきですか?
Railsが画面を描くなら通常モード、画面をReactやスマホアプリが描きRailsはJSONを返すだけならAPIモードです。APIモードはCookie・セッション・respond_to が外れるので、後からHTML画面を足す予定があるなら通常モードのままJSONを返すほうが手戻りが少なくなります。
Rails APIとReactは同じサーバーで動かせますか?
開発中はRails(3000番)とVite(5173番)を別々に動かすのが一般的で、その場合はオリジンが違うので rack-cors での許可か、Viteのproxyが必要です。本番ではリバースプロキシで同じドメインの下にまとめる構成も取れます。
Rails APIの認証は何を使えばよいですか?
単一APIで追加gemを抑えるなら、Rails 8標準の認証ジェネレータをBearerトークン方式に改変する方法があります。有効期限や失効処理を自分で保守することが前提です。複数のサービスで同じトークンを検証するなら devise-jwt を使います。どちらもAPIモードでは追加の設定が要り、そのままでは500や406になります。
独学でReactとRails APIを学ぶなら何から始めればよいですか?
Rails単体でscaffoldを作りcurlで201・422を確かめる、rack-corsを入れてReactから一覧を表示する、認証を足す、最後にDocker Composeへ移す、の順が止まりにくい進め方です。最初からDockerと認証を同時に入れると、エラーがどの層で出ているのか切り分けられなくなります。
APIモードはどのRailsバージョンから使えますか?
APIモード(--api と config.api_only)はRails 5.0で標準機能になりました。2026年10月時点の最新安定版はRails 8.1.4(2026年9月24日リリース)で、本記事のコードと実行結果はこの版のものです。