Introducing A Monorepo Boilerplate for Building Rails APIs with Vite + React SPA

hoshinotsuyoshi

hoshino tsuyoshi

Posted on November 2, 2024

Introducing A Monorepo Boilerplate for Building Rails APIs with Vite + React SPA

Introducing rails-api-vite-easy-stack: A Monorepo Boilerplate for Building Rails APIs with Vite + React SPA

I’m thrilled to introduce my latest passion project, rails-api-vite-easy-stack, a monorepo boilerplate designed to streamline development for those who love Rails and are excited by the simplicity and speed of Vite + React SPAs. While this project draws on my experience as a seasoned Rails engineer, it represents a personal exploration into combining these technologies rather than a production-ready setup. So, consider this an open invitation to explore, experiment, and customize as you see fit.

🏗️ Project Structure

Here's a quick look at the project layout:

  • Backend: A Rails application running in API mode with a GraphQL API. The setup leverages Docker and RSpec for development and testing.
  • Frontend: A Vite-powered React SPA using Bun as the package manager, ensuring fast builds and smooth developer experience.
  • GraphQL Schema Management: Organized separately to simplify backend-frontend type synchronization.

The entire monorepo structure can be summarized like this:

.
├── backend                  # Rails GraphQL API
├── frontend                 # Vite + React SPA
└── graphql-schema           # Shared GraphQL schema
Enter fullscreen mode Exit fullscreen mode

🚀 Backend Highlights

The backend is a Rails API that comes with:

  1. Authentication: Leveraging the new Rails 8 authentication generator. It’s tailored for API mode, using cookie-based sessions managed via ActionController::Cookies for simplicity and security.
  2. Static Asset Handling: The StaticController is used to serve the SPA’s index.html, enabling seamless client-side routing.
  3. Custom Signup Flow: Since the Rails 8 generator doesn’t handle signups out of the box, I implemented a custom flow reminiscent of devise, complete with email verification and password setup.

Here's a peek at how the backend's static asset management is handled:

# config/routes.rb
[
  "/login",
  "/me",
  "/signup"
].each { get _1, to: "static#index" }
Enter fullscreen mode Exit fullscreen mode
# app/controllers/static_controller.rb
class StaticController < ApplicationController
  def index
    render plain: Rails.public_path.join('index.html').read, layout: false
  end
end
Enter fullscreen mode Exit fullscreen mode

⚡ Frontend Features

The frontend is built with Vite and React, and here’s why it’s awesome:

  1. Live Reloading: Thanks to Vite’s development server, frontend changes are reflected instantly.
  2. GraphQL Type Safety: Using graphql-codegen, the frontend automatically generates TypeScript types from the GraphQL schema, ensuring type-safe queries and mutations.

To start the frontend, use:

# cd ./frontend
bun run dev
Enter fullscreen mode Exit fullscreen mode

Proxy Setup for Development

To avoid cross-origin issues, Vite’s proxy forwards API requests to the Rails backend:

// vite.config.ts
server: {
  proxy: {
    '/graphql': 'http://localhost:3000',
  },
}
Enter fullscreen mode Exit fullscreen mode

🧩 Synchronizing Backend and Frontend

Keeping backend and frontend in sync is a breeze with this workflow:

  1. Update GraphQL Schema: Generate the schema in the backend:
   bin/rails graphql:schema:idl
Enter fullscreen mode Exit fullscreen mode
  1. Generate TypeScript Types: Use graphql-codegen in the frontend:
   bun run graphql-codegen
Enter fullscreen mode Exit fullscreen mode

This integration ensures that both backend and frontend speak the same language.

🛠️ System Tests

System tests in the Rails backend use Capybara and Puma, simulating end-to-end flows. Here’s an example test for the signup process:

it 'signup -> mail verification -> set password' do
  visit '/login'
  expect(page).to have_content('Login')
  click_link 'Create an account'

  fill_in "email", with: email
  click_button "Sign up"
  expect(page).to have_content('Inviting')

  perform_enqueued_jobs(only: ActionMailer::MailDeliveryJob)
  mail_message = ActionMailer::Base.deliveries.sole
  url = URI.parse(extract_a_href_from_message(mail_message:))
  visit url.request_uri

  fill_in "password", with: SecureRandom.alphanumeric
  click_button "Set Password"
  expect(page).to have_content("hello, It's me!")
end
Enter fullscreen mode Exit fullscreen mode

For more examples, check the spec/system directory in the repo.

📦 Deployment

Though the deployment process is still evolving, here are the essentials:

  1. Build the Frontend:
   # cd ./frontend
   bun run build:move
Enter fullscreen mode Exit fullscreen mode
  1. Build the Backend Docker Image:
   # cd ./backend
   docker build -t my-spa .
Enter fullscreen mode Exit fullscreen mode

The built frontend assets are synced into the Rails public directory, making it easy for Rails to serve the entire SPA.

🙏 A Word of Caution

This boilerplate is a starting point, not a silver bullet. Please customize and test thoroughly before considering it for production use. As always, feedback and contributions are welcome!

For the complete code and to dive deeper, visit rails-api-vite-easy-stack.

Thank you for taking the time to read about rails-api-vite-easy-stack! I’d love to hear your thoughts on this approach. Do you have any questions about the setup or ideas for improving it? Perhaps you've tried something similar and have insights or challenges to share. Feel free to drop your comments below—I’m looking forward to an engaging discussion and learning from your experiences!"

Happy coding! ✨

💖 💪 🙅 🚩
hoshinotsuyoshi
hoshino tsuyoshi

Posted on November 2, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related