🛠 ステップ1:Rails APIモードアプリの新規作成
まずは以下のコマンドで、APIモード+PostgreSQLのRailsアプリを作成します。
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に以下を追記
gem "bcrypt", "~> 3.1.7" # password digest 用
gem "jwt" # JWT署名・検証用(バージョン指定不要)
gem "rack-cors" # CORS対応(バージョン指定不要)
gem "vite_ruby", "3.2.2" # Vite + Rails連携用 Gem
bundle install
✅ package.json(Vite + React セットアップ用)
次に以下のコマンドで vite と react を導入します(npm使用):
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 を作成:
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',
},
});
↓ エラーがあり修正
Error: Cannot find module 'vite_ruby/plugin'
Vite の設定ファイル vite.config.ts の中でvite_ruby/plugin をインポートしようとしてるけど、vite_ruby は npm パッケージではないので見つからないというエラーです。
↓ 修正内容
📄 vite.config.ts 修正版(Rails APIモード + React専用構成)
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追記 さらに修正
これで最終的にやっと表示確認ができた。
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 で読み込めば反映されます:
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 テーブル
bin/rails g model RoleCategory name:string sort_order:integer
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 テーブル
bin/rails g model Role name:string sort_order:integer role_category:references
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 テーブル
bin/rails g model Industry name:string sort_order:integer
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 テーブル
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ファイルの書き換え
※上記コマンドから少しカラム内容変更
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
✅ モデルファイル
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
✅ マイグレーションを実行
bin/rails db:create db:migrate
✅ ステップ6-①: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
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
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トークンは以下の設定で保存されます:
# 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
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
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 を指定しています。
📝📝自分メモ
下記はよく使うと思うのでどこかで共通として持った方が良いかも
# Cookie にトークンを保存(環境に合わせたセキュリティ設定付き)
cookies.encrypted[:jwt] = {
value: token,
httponly: true,
secure: Rails.env.production?,
same_site: :lax
}
✅ステップ6-④:ルーティング設定(/api/signup の登録)
📁作成場所: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::RegistrationsController の create アクションにルーティング |
ステップ6-⑤:Api::SessionsController
✅ 目的
- メールアドレスとパスワードでログインする
- 成功時に JWT を発行して Cookie に保存
- 失敗時はエラーメッセージを返す
🛠 コントローラ生成コマンド
bin/rails g controller api/sessions create --skip-assets --skip-helper --no-test-framework
✍️ ファイル: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 に保存(セキュリティ設定あり) |
✅ ルーティング追記
namespace :api do
post '/signup', to: 'registrations#create'
post '/login', to: 'sessions#create' # ← 追加
end
ステップ6-⑥:/api/logout(ログアウト処理)
✅ 目的
- Cookieに保存されたJWT(と後で追加するrefresh_token)を破棄する
- フロント側はその後ログイン状態を解除する
🛠 コントローラ修正(Api::SessionsController に destroy アクションを追加)
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からログイン中のユーザー情報を取得
- ログイン状態の確認や、初期表示に使えるようにする
🛠 コントローラ作成
bin/rails g controller api/users show --skip-assets --skip-helper --no-test-framework
📄 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
✅ ルーティング追加
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
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
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のインストール
✅ インストールコマンド
npm install react-router-dom@6
↓ 自動で記述が入る。
"dependencies": {
"react-router-dom": "^6.30.1",
},
※@6 をつけることで、明示的にバージョン6を指定しています
ここでpackage.jsonの整理
1.バージョンを完全固定
^(スキャレット)を外す!!
"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.依存を一度リセット → 再構築
rm -rf node_modules package-lock.json
npm install
npm install で package-lock.json が改めて生成されます(=ロックファイルの正規化)
3.コマンド npm run 〇〇 用の設定
〇〇=devやbuild など
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
📄 ファイル: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から返す
<!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起動)
npm run dev
rails s
↓ じゃなくて
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 設定してたよね)
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 も入る
つまり:
npm run build
これを実行すると…
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)
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
すると以下が生成されます:
tailwind.config.js
postcss.config.js