Ruby on Rails

Rails APIとは?作成手順・JWT認証・React連携・Docker環境を実装解説【Rails 8.1】

Rails APIとは?作成手順・JWT認証・React連携・Docker環境を実装解説【Rails 8.1】

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-jwt 0.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日リリース)で、本記事のコードと実行結果はこの版のものです。

関連記事

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

一創の公式 SNS

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

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

ほか 4 件の記事からもリンクされています。

資料請求

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

  1. 2026.10.29 テックブログ ソニー生命の不正アクセス:38.5万件の対象確認と窓口、停止中の手続き
  2. 2026.10.28 テックブログ リンツ不正アクセス:314万件の対象確認とパスワード強制リセット後の対処
  3. 2026.01.30 コラム 税務調査は個人と法人で何が違う?担当部署・件数・調べられる点と追徴税額を比較【令和6事務年度】
  4. 2026.10.27 テックブログ Joshin(ジョーシン)不正アクセス:最大600万件の対象とサブスク初期パスワードの対処
  5. 2026.10.31 テックブログ 無印良品の情報漏洩:MUJIアプリ最大2,726万件の対象確認となりすましメール対策

RELATED POSTS 関連記事

目次