Rails
The Rails filter transforms idiomatic Rails code into JavaScript, enabling Rails applications to run in browsers and JavaScript runtimes. It handles models, controllers, routes, database schema, seeds, and logging.
Overview
The Rails filter enables a powerful workflow: write standard Rails code that transpiles to browser-ready JavaScript. The same Ruby source can run on the server (with PostgreSQL) and in the browser (with IndexedDB).
app/models/article.rb → dist/models/article.js
app/controllers/... → dist/controllers/...
app/views/...html.erb → dist/views/...js
config/routes.rb → dist/routes.js
db/schema.rb → dist/schema.js
See the Ruby2JS on Rails blog post for a complete walkthrough.
Models
Transforms ActiveRecord model classes with associations, validations, callbacks, and scopes.
Associations
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
belongs_to :author, optional: true
end
import ApplicationRecord from "./application_record.js";
import Comment from "./comment.js";
import Author from "./author.js";
export class Article extends ApplicationRecord {
static table_name = "articles";
get comments() {
let _id = this.id;
return Comment.where({article_id: _id})
}
get author() {
return this._attributes["author_id"]
? Author.find(this._attributes["author_id"])
: null
}
async destroy() {
for (let record of await(this.comments)) {
await record.destroy()
};
return await super.destroy()
}
}
Supported association options:
has_many—:class_name,:foreign_key,:dependentbelongs_to—:class_name,:foreign_key,:optionalhas_one—:class_name,:foreign_key
Validations
class Article < ApplicationRecord
validates :title, presence: true
validates :body, length: { minimum: 10 }
validates :status, inclusion: { in: %w[draft published] }
end
export class Article extends ApplicationRecord {
static _validations = {
title: {presence: true},
body: {length: {minimum: 10}},
status: {inclusion: {in: ["draft", "published"]}}
};
}
Supported validations: presence, length, format, inclusion, exclusion, numericality, uniqueness
Callbacks
class Article < ApplicationRecord
before_save :normalize_title
after_create :notify_subscribers
private
def normalize_title
self.title = title.strip.titleize
end
end
export class Article extends ApplicationRecord {
static _callbacks = {
before_save: ["normalize_title"],
after_create: ["notify_subscribers"]
};
normalize_title() {
this.title = title.trim().titleize()
}
}
Supported callbacks: before_validation, after_validation, before_save, after_save, before_create, after_create, before_update, after_update, before_destroy, after_destroy
Scopes
class Article < ApplicationRecord
scope :published, -> { where(status: 'published') }
scope :recent, -> { order(created_at: :desc).limit(10) }
end
export class Article extends ApplicationRecord {
static published() {
return this.where({status: "published"})
}
static recent() {
return this.order({created_at: "desc"}).limit(10)
}
}
Controllers
Transforms Rails controllers to JavaScript modules with async action 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 show
render 'articles/show', article: @article
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
def article_params
params.require(:article).permit(:title, :body)
end
end
import Article from "../models/article.js";
import * as views from "../views/articles/index.js";
export const ArticlesController = {
before_action: {
set_article: ["show", "edit", "update", "destroy"]
},
async index() {
let articles = await Article.all();
return views.index({articles})
},
async show(id) {
let article = await this.set_article(id);
return views.show({article})
},
async create(params) {
let article = new Article(params.article);
if (await article.save()) {
return {redirect_to: `/articles/${article.id}`}
} else {
return views.$new({article})
}
},
async set_article(id) {
return await Article.find(id)
}
};
Key transformations:
- Controller class → exported module object
- Instance methods → async functions
@ivarassignments → local variablesparams[:id]→ function parameterrender→ view function callredirect_to→ redirect objectnewaction →$new(reserved word)
Routes
Transforms config/routes.rb to a JavaScript router configuration.
Rails.application.routes.draw do
root 'articles#index'
resources :articles do
resources :comments, only: [:create, :destroy]
end
end
import { Router } from "./router.js";
import * as ArticlesController from "./controllers/articles_controller.js";
import * as CommentsController from "./controllers/comments_controller.js";
export const Routes = {
routes: [
{method: "GET", path: "/", action: ArticlesController.index},
{method: "GET", path: "/articles", action: ArticlesController.index},
{method: "GET", path: "/articles/new", action: ArticlesController.$new},
{method: "POST", path: "/articles", action: ArticlesController.create},
{method: "GET", path: "/articles/:id", action: ArticlesController.show},
{method: "GET", path: "/articles/:id/edit", action: ArticlesController.edit},
{method: "PATCH", path: "/articles/:id", action: ArticlesController.update},
{method: "DELETE", path: "/articles/:id", action: ArticlesController.destroy},
{method: "POST", path: "/articles/:article_id/comments", action: CommentsController.create},
{method: "DELETE", path: "/articles/:article_id/comments/:id", action: CommentsController.destroy}
],
articles_path() { return "/articles" },
new_article_path() { return "/articles/new" },
article_path(id) { return `/articles/${id}` },
edit_article_path(id) { return `/articles/${id}/edit` }
};
Supported route methods: root, resources, resource, get, post, patch, put, delete, namespace, scope
Supported options: :only, :except, :path, :as
Schema
Transforms db/schema.rb to JavaScript for IndexedDB or SQLite setup.
ActiveRecord::Schema.define(version: 2024_01_15_000000) do
create_table "articles", force: :cascade do |t|
t.string "title", null: false
t.text "body"
t.string "status", default: "draft"
t.timestamps
end
create_table "comments", force: :cascade do |t|
t.references "article", null: false, foreign_key: true
t.text "body"
t.timestamps
end
end
export const Schema = {
version: 2024_01_15_000000,
tables: {
articles: {
columns: {
id: {type: "integer", primaryKey: true, autoIncrement: true},
title: {type: "string", null: false},
body: {type: "text"},
status: {type: "string", default: "draft"},
created_at: {type: "datetime"},
updated_at: {type: "datetime"}
}
},
comments: {
columns: {
id: {type: "integer", primaryKey: true, autoIncrement: true},
article_id: {type: "integer", null: false, references: "articles"},
body: {type: "text"},
created_at: {type: "datetime"},
updated_at: {type: "datetime"}
}
}
}
};
Supported column types: string, text, integer, float, decimal, boolean, date, datetime, time, binary, references
Seeds
Transforms db/seeds.rb to a JavaScript module.
Article.create!(title: "Welcome", body: "Hello, world!")
Article.create!(title: "Getting Started", body: "Let's begin...")
import Article from "./models/article.js";
export async function run() {
await Article.create({title: "Welcome", body: "Hello, world!"});
await Article.create({title: "Getting Started", body: "Let's begin..."});
}
Logger
Maps Rails logger calls to console methods.
Rails.logger.debug "Processing request"
Rails.logger.info "User logged in"
Rails.logger.warn "Rate limit approaching"
Rails.logger.error "Failed to save"
console.debug("Processing request");
console.info("User logged in");
console.warn("Rate limit approaching");
console.error("Failed to save");
Runtime Requirements
The transpiled JavaScript requires runtime implementations of:
- ApplicationRecord — Base model class with
find,where,create,save,destroy, etc. - ApplicationController — Base controller with routing integration
- Router — URL matching and History API integration
These are provided by the Ruby2JS on Rails demo runtime, which uses:
| Component | Browser | Server |
|---|---|---|
| Database | Dexie (IndexedDB) | better-sqlite3, pg |
| Router | History API | HTTP server |
| Renderer | DOM manipulation | HTML string |
Usage with Other Filters
The Rails filter works best with these companion filters:
Ruby2JS.convert(source, filters: [:rails, :esm, :functions, :active_support])
| Filter | Purpose |
|---|---|
| esm | ES module imports/exports |
| functions | Ruby → JS method mappings (.each → for...of, etc.) |
| active_support | blank?, present?, try, etc. |
| erb | ERB templates → render functions |
| camelCase | Convert snake_case identifiers |
Limitations
The goal is enabling offline-first applications and static deployment, not replacing Rails entirely.