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


I’m assuming you already have:

If you don’t have them, take a look in my previous post.

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.


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 = [
  '', # Dev Frontend
  'http://localhost:5173', # Dev Frontend
  '', # 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]

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 -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:
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 || '';

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(
  }, [])

  return (
    posts.length === 0 
      ? <p>No posts</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:

    get "*path", to: static("index.html")
    post "*path", to: static("index.html")


  • 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