1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Task(Rails API + Vite React SPA)

Last updated at Posted at 2025-05-26

🛠 ステップ1:Rails APIモードアプリの新規作成

まずは以下のコマンドで、APIモード+PostgreSQLのRailsアプリを作成します。

terminal
rails _7.1.3_ new TaskNote --api -d postgresql
cd TaskNote

🛠 ステップ2:初期バージョン構成(Gemfile / package.json)

✅ Ruby / Rails / Node / npm / React / Vite 構成まとめ

コンポーネント バージョン
Ruby 3.2.2
Rails 7.1.3
Node.js 18.x.x(LTS)
npm 9.x.x
React 18.2.0
Vite 4.5.2
vite_ruby gem 3.2.2
PostgreSQL 14

✅ 注意点
node_modules → .gitignore に追加
package-lock.json はGitで管理(npm ci で完全再現可能)

🛠 ステップ3:認証とトップページの構成方針

🔐 認証仕様(JWTベース)
has_secure_password(bcrypt)
JWT+Refreshトークン構成
Cookieに HttpOnly + Secure + SameSite=Lax
🎯 実装ゴール
/api/signup で新規登録
/api/login でログイン&JWT発行(Cookie保存)
/api/logout でCookie削除
/api/current_user でログイン中のユーザー取得
/top にReactでトップページ表示

🛠 ステップ4:次に必要なファイル類

以下の順にコードを整備していきます:
Gemfile と vite.config.ts の設定(React + Vite)
Userモデルとマイグレーション(password_digest付き)
JWTユーティリティ(lib/json_web_token.rb)
Sessions / Registrations コントローラ
Reactエントリーポイント(例:app/javascript/packs/index.jsx)
フロントのLogin / Signup / Logout コンポーネント

✅ Gemfileに以下を追記

gemfile
gem "bcrypt", "~> 3.1.7"      # password digest 用
gem "jwt"                     # JWT署名・検証用(バージョン指定不要)
gem "rack-cors"              # CORS対応(バージョン指定不要)
gem "vite_ruby", "3.2.2"     # Vite + Rails連携用 Gem
terminal
bundle install

✅ package.json(Vite + React セットアップ用)

次に以下のコマンドで vite と react を導入します(npm使用):

terminal
npm init -y

# 必須パッケージをバージョン固定でインストール
npm install react@18.2.0 react-dom@18.2.0
npm install vite@4.5.2
npm install @vitejs/plugin-react --save-dev

✅ 補足:package-lock.json はGitにコミットしてロック管理してください。

✅ vite.config.ts

プロジェクトルートに vite.config.ts を作成:

vite.config.ts
import { defineConfig } from 'vite';
import RubyPlugin from 'vite_ruby/plugin';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    RubyPlugin(),
    react(),
  ],
  server: {
    origin: 'http://localhost:3000',
  },
});

↓ エラーがあり修正

terminal
Error: Cannot find module 'vite_ruby/plugin'

Vite の設定ファイル vite.config.ts の中でvite_ruby/plugin をインポートしようとしてるけど、vite_ruby は npm パッケージではないので見つからないというエラーです。

↓ 修正内容

📄 vite.config.ts 修正版(Rails APIモード + React専用構成)

vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// vite_ruby は使わない前提で修正
export default defineConfig({
  plugins: [react()],
  server: {
    origin: 'http://localhost:3000',
  },
});

✅ なぜこれでよいの?

  • あなたのアプリは Rails API モード(HTMLは使わない or 最小限)
  • React は Vite 経由で動作しており、Gem 側の vite_ruby を必要としない
  • 代わりに vite.config.ts は Vite のネイティブ構成だけで済ませる

↓ 250527追記 さらに修正

これで最終的にやっと表示確認ができた。

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  root: 'app/javascript', // JSXなどのルート
  build: {
    outDir: '../../public', // ✅ public配下にビルドファイルを出力
    emptyOutDir: true,
  },
  resolve: {
    alias: {
      '@': '/app/javascript',
    },
  },
  server: {
    port: 3000,
    proxy: {
      '/api': 'http://localhost:3001',
    },
  },
})

📂 app/javascript/ をプロジェクトのスタート地点(= Viteのワーキングディレクトリ)として扱います。

✅ フォルダ構成(React関連)

app/javascript/
├── index.jsx          ← Reactのエントリポイント
├── index.html         ← ViteのエントリHTML
├── styles/
│   └── tailwind.css   ← TailwindやCSSの管理
├── components/
│   ├── App.jsx        ← 全体ラップコンポーネント
│   ├── Header.jsx
│   ├── Login.jsx
│   └── Signup.jsx

✅ app/javascript/components にReactコンポーネントを全部入れる形でOK!
そして index.jsx で読み込めば反映されます:

index.jsx
import App from './components/App';

✅ 認証エンドポイント(ステップ3補足修正)

🎯 目指す認証関連エンドポイント構成は以下の通り:

機能 メソッド パス 備考
ユーザー登録 POST /api/signup bcrypt + JWT発行(Cookie保存)
ログイン POST /api/login JWT発行(Cookie保存)
ログアウト DELETE /api/logout Cookie削除
ログイン確認 GET /api/current_user JWTからuser取得

🧩 ステップ5:Userモデルと関連モデルの作成

User(ユーザー),RoleCategory(職種カテゴリ),Role(職種),Industry(業種)の各モデルを作成する。
※Usersのマイグレーションファイル名が一番古い(=最初に実行される)タイムスタンプになっていると、先に roles テーブルがない状態で外部キー制約をつけようとしてエラーになってしまう為、関連モデルから順に作成していく。

🛠 1. role_categories テーブル

terminal
bin/rails g model RoleCategory name:string sort_order:integer
db/migrate/xxxxx_create_role_categories.rb
class CreateRoleCategories < ActiveRecord::Migration[7.1]
  def change
    create_table :role_categories do |t|
      t.string :name, null: false
      t.integer :sort_order, null: false, default: 0
      t.timestamps
    end

    add_index :role_categories, :sort_order
  end
end

🛠 2. roles テーブル

terminal
bin/rails g model Role name:string sort_order:integer role_category:references
db/migrate/xxxxx_create_roles.rb
class CreateRoles < ActiveRecord::Migration[7.1]
  def change
    create_table :roles do |t|
      t.string :name, null: false
      t.references :role_category, null: false, foreign_key: true
      t.integer :sort_order, null: false, default: 0
      t.timestamps
    end

    add_index :roles, :sort_order
  end
end

🛠 3. industries テーブル

terminal
bin/rails g model Industry name:string sort_order:integer
db/migrate/xxxxx_create_industries.rb
class CreateIndustries < ActiveRecord::Migration[7.1]
  def change
    create_table :industries do |t|
      t.string :name, null: false
      t.integer :sort_order, null: false, default: 0
      t.timestamps
    end

    add_index :industries, :sort_order
  end
end

🛠 4. Users テーブル

terminal
bin/rails g model User name:string{20} password_digest:string is_admin:boolean email:string \
  prefecture:string city:string utm_source:string utm_medium:string \
  has_project_plan:boolean has_wbs_plan:boolean has_file_upload_plan:boolean \
  has_full_package_plan:boolean has_annual_plan:boolean is_invited_user:boolean \
  role:references industry:references custom_role_description:string

✅ Migrationファイルの書き換え

※上記コマンドから少しカラム内容変更

db/migrate/xxxxxxxx_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :name, limit: 20, null: false
      t.string :password_digest, null: false
      t.boolean :is_admin, null: false, default: false

      t.string :email, null: false
      t.string :prefecture
      t.string :city
      t.string :utm_source
      t.string :utm_medium

      # プラン系(初期値はすべて false)
      t.boolean :has_project_plan, default: false
      t.boolean :has_wbs_plan, default: false
      t.boolean :has_file_upload_plan, default: false
      t.boolean :has_full_package_plan, default: false
      t.boolean :has_annual_plan, default: false
      t.boolean :is_invited_user, default: false

      # 職種・業種・自由記述
      t.references :role, null: false, foreign_key: true
      t.references :industry, foreign_key: true
      t.string :custom_role_description

      t.timestamps
    end

    add_index :users, :email, unique: true
  end
end

✅ モデルファイル

app/models/user.rb
class User < ApplicationRecord
  has_secure_password

  belongs_to :role
  belongs_to :industry, optional: true

  validates :name, presence: true, length: { maximum: 20 }
  validates :email, presence: true, uniqueness: true
end

✅ マイグレーションを実行

terminal
bin/rails db:create db:migrate

✅ ステップ6-①:lib/json_web_token.rb の作成

📂 作成先:lib/json_web_token.rb

lib/json_web_token.rb
class JsonWebToken
  SECRET_KEY = Rails.application.credentials.secret_key_base

  def self.encode(payload, exp = 1.hour.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET_KEY)
  end

  def self.decode(token)
    decoded = JWT.decode(token, SECRET_KEY)[0]
    HashWithIndifferentAccess.new(decoded)
  rescue JWT::DecodeError
    nil
  end
end

🔧 補足:lib/ をautoload対象にする設定

📂 編集するファイル:config/application.rb

config/application.rb
module TaskNote
  class Application < Rails::Application
    config.load_defaults 7.1

    # 追加!lib配下のクラスも自動読み込み対象にする
    config.eager_load_paths << Rails.root.join("lib")
  end
end

✅ ステップ6-②:ApplicationController に current_user / authenticate_user! を定義

📄 ファイル:app/controllers/application_controller.rb

app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include ActionController::Cookies

  def current_user
    return nil unless cookies.encrypted[:jwt]

    decoded = JsonWebToken.decode(cookies.encrypted[:jwt])
    @current_user ||= User.find_by(id: decoded[:user_id]) if decoded
  end

  def authenticate_user!
    render json: { error: 'Unauthorized' }, status: :unauthorized unless current_user
  end
end

✅ 補足解説

メソッド名 説明
current_user Cookieに入っているJWTトークンをデコードして、ユーザーを取得します
authenticate_user! 未ログインなら401 Unauthorized を返します。ログインが必要なAPIで before_action で使えます

🔐 Cookieの構成

この後の /signup /login でも同じく、JWTトークンは以下の設定で保存されます:

/signupや/login
# JWTトークン関連の記述
cookies.encrypted[:jwt] = {
  value: token,
  httponly: true,
  secure: Rails.env.production?,
  same_site: :lax
}

ステップ6-③:Api::RegistrationsController

このコントローラは、/api/signup エンドポイントにアクセスされた際に、ユーザーの新規登録を処理し、成功時にJWTトークンを発行してCookieに保存する役割を果たします。

📁作成場所:app/controllers/api/registrations_controller.rb

terminal
bin/rails g controller api/registrations create --skip-assets --skip-helper --no-test-framework
  • ✅ --skip-assets:APIモードでは不要なCSS/JS関連をスキップ
  • ✅ --skip-helper:viewヘルパーも不要
  • ✅ --no-test-framework:今はテスト不要ならこれも付けてOK
app/controllers/api/registrations_controller.rb
class Api::RegistrationsController < ApplicationController
  # POST /api/signup
  def create
    user = User.new(user_params)
    if user.save
      # JWT トークンの生成
      token = JsonWebToken.encode(user_id: user.id)
      
      # Cookie にトークンを保存(環境に合わせたセキュリティ設定付き)
      cookies.encrypted[:jwt] = {
        value: token,
        httponly: true,
        secure: Rails.env.production?,
        same_site: :lax
      }
      
      render json: { user: user.slice(:id, :email, :name) }, status: :created
    else
      render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
    end
  end

  private

  def user_params
    params.require(:user).permit(
      :name, :email, :password, :password_confirmation,
      :role_id, :industry_id, :custom_role_description
    )
  end
end

✅ 補足ポイント

  • エンドポイント
    このコントローラは POST リクエストで /api/signup を処理します。
    ※ ルーティングは後ほど設定します。

  • ユーザー登録処理
    入力されたパラメータ(user_params)で新しいユーザーを作成します。
    成功すれば、JWTトークンを生成し、Cookieに保存しつつ成功レスポンスを返します。
    失敗すれば、エラーメッセージとともに 422 エラーを返します。

  • セキュリティ設定
    Cookie は httponly(JavaScript からアクセス不可)、secure(本番環境でのみ HTTPS)および same_site: :lax を指定しています。

📝📝自分メモ

下記はよく使うと思うのでどこかで共通として持った方が良いかも

ruby
# Cookie にトークンを保存(環境に合わせたセキュリティ設定付き)
cookies.encrypted[:jwt] = {
  value: token,
  httponly: true,
  secure: Rails.env.production?,
  same_site: :lax
}

✅ステップ6-④:ルーティング設定(/api/signup の登録)

📁作成場所:config/routes.rb

config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    post '/signup', to: 'registrations#create'
  end
end

✅ 説明

内容 説明
namespace :api app/controllers/api/ ディレクトリ配下のコントローラを使う
post '/signup' POSTメソッドで /api/signup にリクエストが来たとき
to: 'registrations#create' Api::RegistrationsControllercreate アクションにルーティング

ステップ6-⑤:Api::SessionsController

✅ 目的

  • メールアドレスとパスワードでログインする
  • 成功時に JWT を発行して Cookie に保存
  • 失敗時はエラーメッセージを返す

🛠 コントローラ生成コマンド

terminal
bin/rails g controller api/sessions create --skip-assets --skip-helper --no-test-framework

✍️ ファイル:app/controllers/api/sessions_controller.rb

app/controllers/api/sessions_controller.rb
class Api::SessionsController < ApplicationController
  # POST /api/login
  def create
    user = User.find_by(email: params[:email])

    if user&.authenticate(params[:password])
      token = JsonWebToken.encode(user_id: user.id)

      cookies.encrypted[:jwt] = {
        value: token,
        httponly: true,
        secure: Rails.env.production?,
        same_site: :lax
      }

      render json: { user: user.slice(:id, :email, :name) }, status: :ok
    else
      render json: { error: 'メールアドレスまたはパスワードが違います' }, status: :unauthorized
    end
  end
end

✅ 注意点

項目 内容
authenticate has_secure_password により、自動的にパスワード検証される
&.(ぼっち演算子) userがnilでも authenticate を呼んでエラーにならない
JWTの保存 /signup 同様に Cookie に保存(セキュリティ設定あり)

✅ ルーティング追記

routes.rb
namespace :api do
  post '/signup', to: 'registrations#create'
  post '/login',  to: 'sessions#create'  # ← 追加
end

ステップ6-⑥:/api/logout(ログアウト処理)

✅ 目的

  • Cookieに保存されたJWT(と後で追加するrefresh_token)を破棄する
  • フロント側はその後ログイン状態を解除する

🛠 コントローラ修正(Api::SessionsController に destroy アクションを追加)

Api::SessionsController
class Api::SessionsController < ApplicationController
  # POST /api/login はすでに作成済

  # DELETE /api/logout
  def destroy
    cookies.delete(:jwt)
    cookies.delete(:refresh_token) # refresh_tokenを後で使う場合に備えて
    head :no_content
  end
end

Cookie(JWTとrefresh_token)を削除し、204を返す

ステップ6-⑦:/api/current_user(現在ログイン中のユーザー情報取得)

✅ 目的

  • ブラウザに保存されたJWTからログイン中のユーザー情報を取得
  • ログイン状態の確認や、初期表示に使えるようにする

🛠 コントローラ作成

terminal
bin/rails g controller api/users show --skip-assets --skip-helper --no-test-framework

📄 app/controllers/api/users_controller.rb

app/controllers/api/users_controller.rb
class Api::UsersController < ApplicationController
  before_action :authenticate_user!

  # GET /api/current_user
  def show
    render json: {
      user: {
        id: current_user.id,
        email: current_user.email,
        name: current_user.name,
        is_admin: current_user.is_admin
      }
    }
  end
end

✅ ルーティング追加

routes.rb
namespace :api do
  post   '/signup',        to: 'registrations#create'
  post   '/login',         to: 'sessions#create'
  delete '/logout',        to: 'sessions#destroy'
  get    '/current_user',  to: 'users#show'  # ← 追加
end

✅ ポイント

項目 内容
before_action :authenticate_user! JWTの検証(未ログインは401)
current_user 利用 CookieからJWTを解析して取得済みユーザーを返す
レスポンス React側でログイン状態の確認に使えるよう、最小限のユーザーデータを返す

ステップ7-①:React側 /signup(ユーザー登録)画面の作成

✅ 目的

  • 新規ユーザーが名前・メール・パスワードなどを入力して /api/signup にPOST
  • 成功したらログイン済み扱い(JWTはCookieで自動管理される)
  • エラー時は画面に表示
    📄 ファイル:app/javascript/components/Signup.jsx
app/javascript/components/Signup.jsx
import React, { useState } from 'react';

const Signup = () => {
  const [form, setForm] = useState({
    name: '',
    email: '',
    password: '',
    password_confirmation: '',
    role_id: '',
    industry_id: '',
    custom_role_description: ''
  });

  const [error, setError] = useState(null);

  const handleChange = (e) => {
    setForm({ ...form, [e.target.name]: e.target.value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    const response = await fetch('/api/signup', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      credentials: 'include', // ← Cookie送受信のために必要!
      body: JSON.stringify({ user: form })
    });

    if (response.ok) {
      const data = await response.json();
      alert(`ようこそ、${data.user.name} さん!`);
      window.location.href = '/'; // トップページへリダイレクト
    } else {
      const err = await response.json();
      setError(err.errors?.join(', ') || '登録に失敗しました');
    }
  };

  return (
    <div>
      <h2>新規登録</h2>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <input type="text" name="name" value={form.name} onChange={handleChange} placeholder="名前" required />
        <input type="email" name="email" value={form.email} onChange={handleChange} placeholder="メール" required />
        <input type="password" name="password" value={form.password} onChange={handleChange} placeholder="パスワード" required />
        <input type="password" name="password_confirmation" value={form.password_confirmation} onChange={handleChange} placeholder="確認用パスワード" required />
        {/* 今はrole_id/industry_idは直接IDを入力(後でプルダウンに) */}
        <input type="number" name="role_id" value={form.role_id} onChange={handleChange} placeholder="職種ID" required />
        <input type="number" name="industry_id" value={form.industry_id} onChange={handleChange} placeholder="業種ID(任意)" />
        <input type="text" name="custom_role_description" value={form.custom_role_description} onChange={handleChange} placeholder="その他職種(任意)" />
        <button type="submit">登録</button>
      </form>
    </div>
  );
};

export default Signup;

✅ 注意点

項目 説明
credentials: 'include' Cookie付き通信(JWT保持)には必須!
user: form RailsのStrong Parametersに合わせてネスト
フォーム簡易対応 最初は手打ちでID指定。後で roles / industries をAPIから取得してプルダウンに変える予定

ステップ7-②:React側 /login(ログイン画面)

✅ 目的

  • メールとパスワードを使って /api/login にPOST
  • 成功時はCookieにJWTが保存され、ログイン扱いに
  • エラー時は画面にエラー表示

📄 ファイル:app/javascript/components/Login.jsx

app/javascript/components/Login.jsx
import React, { useState } from 'react';

const Login = () => {
  const [form, setForm] = useState({ email: '', password: '' });
  const [error, setError] = useState(null);

  const handleChange = (e) => {
    setForm({ ...form, [e.target.name]: e.target.value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    const response = await fetch('/api/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      credentials: 'include',
      body: JSON.stringify(form)
    });

    if (response.ok) {
      const data = await response.json();
      alert(`ようこそ、${data.user.name} さん!`);
      window.location.href = '/'; // トップページへ遷移
    } else {
      const err = await response.json();
      setError(err.error || 'ログインに失敗しました');
    }
  };

  return (
    <div>
      <h2>ログイン</h2>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <input type="email" name="email" value={form.email} onChange={handleChange} placeholder="メール" required />
        <input type="password" name="password" value={form.password} onChange={handleChange} placeholder="パスワード" required />
        <button type="submit">ログイン</button>
      </form>
    </div>
  );
};

export default Login;

✅ 注意点

項目 説明
credentials: 'include' JWTのCookie送信に必須
form データ形式 /api/login はプレーンな email / password でOK(ネスト不要)
レスポンス処理 alert & redirect は後で状態管理に置き換え可

ステップ7-③:index.jsx

✅ 前提

  • React Router v6 を使う想定で進めます(今のReact構成なら v6 が一般的)
  • 最低限、/signup, /login, /(仮トップ)をルーティング

react-router-dom v6のインストール

✅ インストールコマンド

terminal
npm install react-router-dom@6

↓ 自動で記述が入る。

package.json
"dependencies": {

    "react-router-dom": "^6.30.1",
},

※@6 をつけることで、明示的にバージョン6を指定しています

ここでpackage.jsonの整理

1.バージョンを完全固定
^(スキャレット)を外す!!

package.json
"dependencies": {
 "react": "^18.2.0",
 "react-dom": "^18.2.0",
 "react-router-dom": "^6.30.1",
 "vite": "^4.5.2"
},
"devDependencies": {
 "@vitejs/plugin-react": "^4.5.0"
}

✅ ^ を外すことで、将来バージョンが自動で上がって壊れるリスクを防げます。

2.依存を一度リセット → 再構築

terminal
rm -rf node_modules package-lock.json
npm install

npm install で package-lock.json が改めて生成されます(=ロックファイルの正規化)

3.コマンド npm run 〇〇 用の設定

〇〇=devやbuild など

package.json
"scripts": {
  "dev": "vite",
  "build": "vite build",
  "preview": "vite preview"
} 

📄 ファイル:app/javascript/packs/index.jsx

app/javascript/packs/index.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Signup from '../components/Signup';
import Login from '../components/Login';

const TopPage = () => <h2>トップページ(仮)</h2>;

const App = () => (
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<TopPage />} />
      <Route path="/signup" element={<Signup />} />
      <Route path="/login" element={<Login />} />
    </Routes>
  </BrowserRouter>
);

const rootElement = document.getElementById('root');
if (rootElement) {
  const root = createRoot(rootElement);
  root.render(<App />);
}

✅ app/views/layouts/application.html.erb(React/Vite向けの最小レイアウト)

React + Vite SPA構成
最初のHTMLだけはRailsから返す

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>TaskNote</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= vite_client_tag %>
    <%= vite_javascript_tag 'index' %>
  </head>

  <body>
    <div id="root"></div>
  </body>
</html>

✅ 補足ポイント

タグ 説明
<%= csrf_meta_tags %> RailsのCSRF対策に必要(必要に応じて)
<%= vite_client_tag %> ViteのHMR(ホットリロード)に必要(開発中)
<%= vite_javascript_tag 'index' %> app/javascript/packs/index.jsx を読み込む(JS入口)
<div id="root"> Reactアプリがマウントされる場所

✅ 動作確認手順(npm起動)

terminal 1
npm run dev
terminal 2
rails s

↓ じゃなくて

terminal 2
rails s -p 3001

✅ 背景

Vite を使った React フロントエンド開発では、vite dev コマンドが 既定でポート 3000 を使うため、Railsもデフォルトのまま起動するとポート競合が発生します。

✅ 開発環境での理想構成(Reactは別ポート)

【開発中】
http://localhost:3000 ← Vite dev server(Reactを開発中に即時表示)
↓
APIリクエスト
↓ proxy
http://localhost:3001 ← Rails API(認証・DB処理など)

✅ vite.config.ts(これで proxy 設定してたよね)

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  root: 'app/javascript', // JSXなどのルート
  build: {
    outDir: '../../public', // ✅ public配下にビルドファイルを出力
    emptyOutDir: true,
  },
  resolve: {
    alias: {
      '@': '/app/javascript',
    },
  },
  server: {
    port: 3000,
    proxy: {
      '/api': 'http://localhost:3001', //👈ココ
    },
  },
})

✅ 本番環境ではどうなるの?
本番では Viteのdevサーバー(3000)は使いません!

✅ 本番構成(Railsだけ)

【本番】
http://tasknote.com
↓
Railsがpublic/index.html(vite build済み)を返す
↓
Reactが読み込まれて起動
↓
Rails API(同一ドメイン)にアクセス
  • Viteで npm run build 済み
  • Reactの静的ファイルは public/ に出力される
  • Rails は1つのポート(例:3000や80)でOK
  • rails s や nginx + puma 構成で動かす

✅ 今後ずっと -p 3001 が必要?

環境 rails s -p 3001 必要? 理由
開発 ✅ 必要 Vite dev server が 3000 を使ってるため
本番 ❌ 不要(むしろ指定すべきでない) Railsだけ動けばよいから 3000 でも 80 でも自由

開発中のおすすめ構成

  • Reactフロント:vite dev(http://localhost:3000)
  • Rails API:rails s -p 3001
  • ReactからのAPI通信:proxyで /api → localhost:3001/api

本番構成(Renderなど)

Viteで npm run build

Railsが public/index.html と public/assets を配信

フロントもAPIも https://tasknote.com で完結

✍️質問タイム

🔸 Q1.

どうして開発中は vite が public/index.html を配信して、本番になると Rails が配信するの?ややこしくない?

✅ A1.

実は逆です!開発中は Vite Dev Server が public/ を無視して、app/javascript/index.html を元に「仮想的に」配信しています。
一方、本番では vite build で public/index.html を生成し、Railsがそのまま返す構成です。

環境 誰が配信? index.html の元データ 備考
開発 Vite Dev Server (vite dev) app/javascript/index.html 即時リロードが効く
本番 Rails (static#index) public/index.html(vite buildの結果) ReactのJSバンドルも含む

つまり開発中はビルド済みの public/ は無視されてます。

🔸 Q2.

今開発では、ターミナル1: npm run dev、ターミナル2: rails s -p 3001 で表示確認してるんだけどそれでも大丈夫?

✅ A2.

完璧な正しい使い方です。

  • npm run dev → React(Vite Dev Server)を起動(localhost:3000)
  • rails s -p 3001 → Rails APIサーバーを起動(localhost:3001)
  • そして vite.config.ts で /api を localhost:3001 に proxy してるので、ReactからAPI通信もスムーズ。

🔸 Q3.

開発での npm run build は一回実行してしまえばその後はしなくてOK?

✅ A3.

開発中は不要です。

  • vite dev で動かしてる間は vite build の内容(public/index.html など)を一切使いません。

  • npm run build は本番用の静的ファイルを生成する目的のみです。

🔸 Q4.

開発での npm run build で作られた public/index.html は絶対に消しちゃいけないっていう感じだよね?

✅ A4.

本番デプロイする前には必要!でも開発中は作り直しOK。

  • npm run build で生成された public/index.html は、本番でRailsが使う唯一のエントリファイルです。
  • 開発中に消しちゃっても、再度 npm run build すれば復活します。
  • なので「絶対に消しちゃいけない」ではなく、「本番には必ず必要」です。

🔸 Q5.

public/assets って今作ってないんだけど、今後どういうものを入れる?

✅ A5.

それも vite build 実行時に自動で生成されます!

public/assets/index-xxxxxxxx.js

public/assets/index-xxxxxxxx.css(もしCSSがあれば)

画像などインポートしてると public/assets/image-xxx.png も入る

つまり:

terminal
npm run build

これを実行すると…

pgsql
public/
├── index.html
└── assets/
    ├── index-abc123.js
    ├── index-def456.css
    └── some-image-xyz789.png

この assets/ を Rails がそのまま静的配信します。

✅ まとめ

開発中は vite dev が仮想サーバーを使い、本番では vite build で生成されたファイルをRailsが配信する ― これがReact×Railsのモダンな分離構成です。

🎨 Q1. Tailwind CSS を使うにはどうする?

Vite + React のプロジェクトでは、TailwindはPostCSS + Vite プラグイン構成で組み込むのが標準です。

✅ 導入手順(npm)

terminal
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

すると以下が生成されます:

tailwind.config.js
postcss.config.js
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?