Rails and Vite
If you just want to use Vite as a replacement for the Rails included Webpacker (as it was up to Rails 6), you can simply use Vite Ruby.
The goal of this article is different: to have 2 separate environments (Rails API and React frontend) using Vite, but with some degree of integration:
- you can use Puma in production to serve the React App (i.e., one container only)
- you can use
node
to automate any other task in the Rails app - start both servers with one single command
Requirements
I’m assuming you already have:
- Node.js (check Vite Compatibility Note)
- Rails
If you don’t have them, you can use nvm
and rvm
or take a look in my previous post about asdf
.
Rails Setup
We’ll call our app blogger
, so create the Rails API with:
rails new blogger --api
cd blogger
rails g scaffold Post title:string content:text
rails db:migrate
Database Seed
Create one single post, appending to db/seeds.rb
:
Post.create!(title: "First post", content: "This is the first post!")
Run rails db:seed
.
CORS
As we’ll be running two different servers at different ports, CORS will block frontend requests to the API. Let’s enable it.
Open Gemfile
and uncomment (or add) the following line:
gem "rack-cors"
Get it installed, so we can configure it:
bundle install
Allow the frontend to query the API editing config/initializers/cors.rb
and appending the following configuration:
# TODO: Add production URLs to the list
# Use '*' if you want expose the API to the world
accept = [
'http://127.0.0.1:5173', # Dev Frontend
'http://localhost:5173', # Dev Frontend
'http://127.0.0.1:5174', # Dev Frontend
'http://localhost:5174', # Dev Frontend
]
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins accept
resource '*',
headers: :any,
methods: %i[get post put delete options patch],
expose: %w[Authorization]
end
end
Foreman Backend Config
Later we’ll use Foreman to start all servers for us. Following its documentation recommends to not include it in the Gemfile, just install it with:
gem install foreman
Create the configuration for it, adding a file named Procfile
to the project root folder with this content:
rails: bundle exec rails s -p 3000
Run bundle install
.
Backend is configured. Time to install and configure the frontend.
Bonus: all above steps in a single take:
rails new blogger --api
cd blogger
rails g scaffold Post title:string content:text
rails db:migrate
echo 'Post.create!(title: "First post", content: "This is the first post!")' >> db/seeds.rb
rails db:seed
bundle add rack-cors
bundle install
printf '\n' >> config/initializers/cors.rb
wget https://gist.githubusercontent.com/raelgc/799cadce1303bd604a38bc3808d272b4/raw/1b401713a728178ed4ab86e061bbe1b0cc4d5033/cors.rb -O ->> config/initializers/cors.rb
gem install foreman
echo 'rails: bundle exec rails s -p 3000' >> Procfile
Vite Setup
npm create vite@latest . -- --template react-ts
If this is the first Vite app you’re creating, this prompt will appear (answer y
):
Need to install the following packages:
create-vite@5.1.0
Ok to proceed? (y)
After it, Vite will ask you what to do about the existing files in the project folder (answer to “Ignore files and continue”):
? Current directory is not empty. Please choose how to proceed: › - Use arrow-keys. Return to submit.
Remove existing files and continue
Cancel operation
❯ Ignore files and continue
Install all configured packages:
npm install
Moving frontend content
Create the folder for the React app and move Vite related files:
mkdir react
mv src react/
mv public react/
mv index.html react/
mv vite.config.ts react/
mkdir public && touch public/.keep
Update tsconfig.node.json
with updated file placement and replace the line "include": ["vite.config.ts"]
with:
"include": ["react/vite.config.ts", "react/src/**/*.d.ts"]
Update package.json
scripts to point to react
folder, replacing the scripts
section with the following:
"scripts": {
"dev": "vite react",
"build": "tsc && vite build react && touch public/.keep",
"lint": "eslint react --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview react"
},
Update react/vite.config.ts
to build using the react/src
folder and publish to the rails public
folder (which will be always clean, aside the .keep
file), adding the following inside defineConfig
function:
// Paste these lines inside defineConfig
build: {
emptyOutDir: true,
outDir: '../public'
},
Update tsconfig.json
with the updated file placement and replace the line "include": ["src"],
with:
"include": ["react/src"],
Foreman Frontend Config
As it’s already installed, let’s just include the dev
script into Foreman (i.e., append to Procfile
):
web: npm run dev
Frontend configuration is ready. Now let’s do some requests to the backend.
Requests to the API
Install axios
to easily make a request to the API:
npm install --save axios
Edit react/src/main.tsx
and configure axios
default base URL, placing the following content above ReactDOM.createRoot
line:
// ... existing imports above
import axios from 'axios';
axios.defaults.baseURL = import.meta.env.VITE_APP_API_URL || 'http://127.0.0.1:3000';
If you’ll serve the API in a different URL, set an Env var for VITE_APP_API_URL
. See how Vite handles Env variables.
Let’s update our React app to list all posts. Replace react/src/App.tsx
entire content with:
import { useState, useEffect } from 'react'
import axios from 'axios'
import './App.css'
type Post = {
id: number,
title: string,
content: string,
}
function App() {
const [posts, setPosts] = useState<Post[]>([])
useEffect(() => {
axios.get('/posts').then(response => setPosts(response.data)).catch(console.error)
}, [])
return (
posts.length === 0
? <p>No posts</p>
: (
<>
<h1>Posts</h1>
{posts.map(post => (
<>
<h2>{post.title}</h2>
<p>{post.content}</p>
</>
))}
</>
)
)
}
export default App
Serving static pages with Rails Server (Puma)
In a production environment, usually the frontend will be served by nginx. But it’s possible to have Rails serving both.
Add the rails-static-router
gem to the Gemfile
:
# Serve static index as catch-all route
gem "rails-static-router"
Install it:
bundle install
Add to config/routes.rb:
if ENV["RAILS_SERVE_STATIC_FILES"].present?
get "*path", to: static("index.html")
post "*path", to: static("index.html")
end
Scripts
- Start backend:
rails s
- Start frontend:
npm run dev
- Start both (as
foreman
is installed):foreman start
- Build for production:
npm run build
If you want to test using rails server
as production web server (usually it’d be nginx) in order to test:
- First run the “Build for production” script
- Then run:
RAILS_SERVE_STATIC_FILES=1 RAILS_ENV=production rails s