Ruby2JS on Rails
Ruby2JS on Rails
This guide covers transpiling Rails applications to JavaScript, enabling Rails patterns to run in browsers and JavaScript runtimes without a Ruby installation.
Table of Contents
- Ruby2JS on Rails
When to Use This Approach
Ruby2JS on Rails is ideal for:
- Offline-first applications — Share validation logic and views between server and browser
- Static deployment — Deploy to GitHub Pages, Netlify, S3, Cloudflare Pages
- Edge computing — Run MVC patterns on Cloudflare Workers or Vercel Edge
- Portable bundles — Distribute Rails-patterned functionality without Ruby dependencies
The transpiled output is compact, native JavaScript with direct access to browser APIs—no runtime library required.
Quick Start
No Ruby installation required. Node.js 22+ is all you need.
curl -L https://www.ruby2js.com/demo/ruby2js-on-rails.tar.gz | tar xz
cd ruby2js-on-rails
npm install
bin/dev
Open http://localhost:3000. The source is Ruby. The runtime is JavaScript.
Project Structure
A Ruby2JS on Rails project mirrors standard Rails conventions:
app/
├── models/
│ ├── application_record.rb
│ ├── article.rb
│ └── comment.rb
├── controllers/
│ ├── application_controller.rb
│ ├── articles_controller.rb
│ └── comments_controller.rb
└── views/
├── layouts/
│ └── application.html.erb
└── articles/
├── index.html.erb
├── show.html.erb
├── new.html.erb
└── edit.html.erb
config/
├── routes.rb
└── database.yml
db/
├── schema.rb
└── seeds.rb
The build process transpiles this to:
dist/
├── models/
│ ├── application_record.js
│ ├── article.js
│ └── comment.js
├── controllers/
│ ├── articles_controller.js
│ └── comments_controller.js
├── views/
│ └── articles/
│ ├── index.js
│ ├── show.js
│ └── ...
├── routes.js
├── schema.js
└── seeds.js
Development Workflow
The workflow mirrors modern JavaScript development:
Development:
bin/dev # Start hot-reload server
Edit a Ruby file, save, and the browser refreshes automatically.
Production:
npm run build # Generate static assets
Deploy the dist/ directory anywhere that serves static files.
What Gets Transpiled
Models
Standard ActiveRecord patterns translate directly:
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
belongs_to :author, optional: true
validates :title, presence: true
validates :body, length: { minimum: 10 }
scope :published, -> { where(status: 'published') }
before_save :normalize_title
private
def normalize_title
self.title = title.strip.titleize
end
end
Controllers
Controllers become JavaScript modules with async functions:
class ArticlesController < ApplicationController
before_action :set_article, only: [:show, :edit, :update, :destroy]
def index
@articles = Article.all
render 'articles/index', articles: @articles
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article
else
render 'articles/new', article: @article
end
end
private
def set_article
@article = Article.find(params[:id])
end
end
Routes
Rails routing DSL works as expected:
Rails.application.routes.draw do
root 'articles#index'
resources :articles do
resources :comments, only: [:create, :destroy]
end
end
ERB Templates
ERB templates become JavaScript render functions:
<h1><%= @article.title %></h1>
<p><%= @article.body %></p>
<h2>Comments</h2>
<% @article.comments.each do |comment| %>
<div class="comment">
<%= comment.body %>
</div>
<% end %>
See the ERB filter documentation for details.
Runtime Architecture
The transpiled JavaScript requires runtime implementations. The demo provides these backed by different storage engines:
Database Options
| Adapter | Runtime | Storage | Use Case |
|---|---|---|---|
| Dexie | Browser | IndexedDB | Offline-first apps |
| sql.js | Browser | SQLite (WASM) | SQL compatibility |
| better-sqlite3 | Node.js | SQLite file | Server deployment |
| pg | Node.js | PostgreSQL | Production server |
Configure in config/database.yml:
development:
adapter: dexie
production:
adapter: better_sqlite3
database: db/production.sqlite3
Server Runtimes
The same transpiled code runs on multiple JavaScript runtimes:
bin/rails server # Browser (default)
bin/rails server --runtime node # Node.js
bin/rails server --runtime bun # Bun
bin/rails server --runtime deno # Deno
Each runtime uses its native HTTP server—no Express or framework overhead.
Developer Experience
Console Logging
Rails-style logging appears in browser DevTools:
Article Create {title: "Hello", body: "World", created_at: "..."}
Article Update {id: 1, title: "Hello", body: "Updated", updated_at: "..."}
Enable “Verbose” logging level to see detailed output.
Sourcemaps
Ruby files appear in browser DevTools sources:
- Set breakpoints on Ruby lines
- Step through Ruby code
- Inspect variables with Ruby names
The sourcemaps connect running JavaScript back to the Ruby source.
Filter Configuration
Typical configuration for Rails transpilation:
Ruby2JS.convert(source,
eslevel: 2022,
filters: [:rails, :esm, :functions, :erb, :active_support]
)
| Filter | Purpose |
|---|---|
| rails | Models, controllers, routes, schema |
| esm | ES module imports/exports |
| functions | Ruby → JS method mappings |
| erb | ERB templates → render functions |
| active_support | blank?, present?, try, etc. |
Limitations
This approach transpiles Rails patterns, not the full Rails framework:
| Works | Doesn’t Work |
|---|---|
| Models with associations | Metaprogramming (method_missing) |
| Validations | Complex SQL queries |
| Callbacks | Action Mailer |
| Controllers | Action Cable (server component) |
| Routes | Active Job |
| ERB templates | Migrations at runtime |
| Logging | eval/exec |
The goal is enabling offline-first applications and static deployment—not replacing Rails entirely.
Dual Runtime Strategy
A powerful pattern: the same Ruby source runs on both server and browser.
Ruby Source
│
┌───────────────┼───────────────┐
▼ ▼ ▼
Ruby Runtime Ruby2JS → Ruby2JS →
│ Browser JS Server JS
│ │ │
▼ ▼ ▼
PostgreSQL IndexedDB SQLite/PG
This enables:
- Single source of truth — Validation logic in one place
- Progressive enhancement — Server renders, browser enhances
- Offline capability — Browser works without server
- Fallback — Server handles what browser can’t
Example: Validation Sharing
Define validations once in the model:
class Article < ApplicationRecord
validates :title, presence: true, length: { maximum: 100 }
validates :body, presence: true
end
The same validation runs:
- On the server (Ruby) before database save
- In the browser (JavaScript) for instant feedback
- Guaranteed consistency—same source, same rules
See Also
- Rails Filter Reference — Complete filter documentation
- ERB Filter — Template transpilation
- ActiveSupport Filter — Rails helper methods
- Demo Source — Complete example project
- Blog Post — Detailed walkthrough