Ruby On Rails 2026
Introduction
Got new job which wanted this skill to poking around to have another go.
Installation
No robot back in 2020 when last I gave this the initial look. It is telling me to use asdf with a specific version. Clearly I need to check this out before doing. For Ubuntu its self it say
sudo apt install -y build-essential libssl-dev libreadline-dev zlib1g-dev libsqlite3-dev
So went with the version of asdf it said because the second instructions fail because there is no completions directory with the latest release so for asdf
sudo apt install curl git
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0
echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc
echo '. "$HOME/.asdf/completions/asdf.bash"' >> ~/.bashrc
source ~/.bashrc
Now for rails went with 3.3.11
asdf plugin add ruby
asdf install ruby 3.3.11
asdf global ruby 3.3.11
And for Bundler and Rails
gem install bundler -v 2.4.19
gem install rails
rails -v Rails 8.1.3
Gemfile Stuff
Gemfile Basics
The Gemfile is Ruby’s equivalent of npm’s package.json. It defines which gems your application depends on and in which environments they should be installed.
Bundler supports groups, which work similarly to npm’s dependencies and devDependencies.
Grouping Gems
You can group gems so they are only installed or loaded in specific environments.
A common example is the development group:
group :development do
gem "web-console"
gem "rubocop", require: false
gem "rubocop-rails", require: false
gem "htmlbeautifier"
end
You can also combine multiple groups:
group :development, :test do
gem "debug", platforms: %i[mri windows], require: "debug/prelude"
gem "bundler-audit", require: false
end
Gems inside these blocks are only installed when Bundler is run in those environments, and Rails only loads them when running in that environment.
Inline Group Syntax
Bundler also supports an inline form, which is functionally identical to the block syntax:
gem "sorbet", group: :development
This line is equivalent to:
group :development do
gem "sorbet"
end
Use whichever style reads better for your Gemfile.
Gems Loaded in All Environments
Any gem not placed inside a group is installed and loaded in every environment (development, test, production).
For example:
gem "sorbet-runtime"
This gem will be available everywhere because it is not wrapped in a group.
Making my Website Page
Getting Started
So set up and ready to go. A few things to note
- routes in /config/routes
- views in /app/views/<page-name>
- layouts in /app/views/layouts
- components in app and a PageHeader component component with
So I generated an app, page and component
rails new bibble_web_ror --css=tailwind
cd bibble_web_ror
rails generate controller Bill index
rails generate component PageHeader
Told the robot what I did for view and it kindly gave me the rest. Here is the amended layout
<body class="min-h-screen bg-(--color-bg-page) text-(--color-text-primary)">
<a href="#main-content" class="sr-only focus:not-sr-only">Skip to content</a>
<main id="main-content"
class="max-w-300 bg-(--color-bg-sections) px-2 pt-2 shadow-(--shadow-page) not-only:mx-auto">
<%= render PageHeaderComponent.new(title: @title, subtitle: @subtitle) %>
<div class="u-container">
<%= yield %>
</div>
</main>
<footer class="p-4 text-center text-sm text-(--color-text-primary)">
© 2025 Iain Wiseman
</footer>
</body>
And for the component
<header class="mb-6">
<h1 class="text-3xl font-bold text-(--color-text-primary)">
<%= @title %>
</h1>
<% if @subtitle.present? %>
<p class="mt-1 text-(--color-text-secondary)">
<%= @subtitle %>
</p>
<% end %>
</header>
The generate component failed because I am on rails 8. Suspect there a billion of these generators but we chose view_component. You need to add it to the gemfile and do a bundle install.
rails generate view_component:erb PageHeader
Next it failed again because ruby expects there to be a .rb file which is not automatically generated. Not sure why but suspect my new employer will let me know. Anyway here it is.
class PageHeaderComponent < ViewComponent::Base
def initialize(title:, subtitle: nil)
@title = title
@subtitle = subtitle
end
end
The robot says you can generate this with
rails generate component PageHeader --ruby
So this did not work so I did something I hate doing, read the docs which show
rails generate view_component:component PageHeader
And of course this did work.
Tailwind
This did not work either. Ended up following the excellent instructions on their site for tailwind which mainly involved.
bundle add tailwindcss-rails
rails tailwindcss:install
I found I had to run with bin/dev as the generation of color did not happen
bin/dev
The config tailwind.config.js needs to be empty like in vue. And the theme stuff is in app/assets/stylesheets/tailwind/application.css
Debugger
This seemed harder than it needed to be. I think there have been several approaches at it which has left some stuff behind. Anyway I used https://mickzijdel.com/blog/2025-05-19-setting-up-a-debugger-for-ruby-on-rails-8/ for my setup. First I installed the vs code extension VSCode rdbg ruby debugger by Koichi Sasada. Next I added these to my gemfile
gem "rdbg" # Ruby debug integration
gem "foreman" # Necessary for the procfile to work
So next was the Procfile.dev in the root of the project. Replace the original web: line with this
web: RUBY_DEBUG_OPEN=TRUE RUBY_DEBUG_NONSTOP=TRUE rdbg --command --open --stop-at-load -- bundle exec bin/rails server -p 3000
css: bin/rails tailwindcss:watch
So here are the files for reference.
First Tasks
{
"version": "2.0.0",
"tasks": [
{
"label": "runBinDev",
"type": "shell",
"command": "bin/dev",
"isBackground": true,
"problemMatcher": [
{
"owner": "custom-rdbg-task",
"pattern": [
{
"regexp": "^.*DEBUGGER: Debugger can attach.*$",
"kind": "info",
"file": 1,
"location": 1,
"message": 0
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "^.*DEBUGGER: Debugger can attach.*$",
"endsPattern": "^.*DEBUGGER: Debugger can attach.*$"
}
}
],
"presentation": {
"reveal": "always",
"panel": "dedicated",
"clear": true
},
"group": {
"kind": "build",
"isDefault": false
}
}
]
}
Now launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "rdbg",
"name": "Run bin/dev & Attach with rdbg",
"request": "attach",
"preLaunchTask": "runBinDev",
"localfs": true,
"localfsMap": "${workspaceFolder}:${workspaceFolder}"
},
{
"type": "rdbg",
"name": "Attach with rdbg",
"request": "attach",
"localfs": true,
"localfsMap": "${workspaceFolder}:${workspaceFolder}"
}
]
}
First Go
Writing a Test Case
So wanted to get a test case going because of the lack of type checking. When you generate a new component you get an empty test case. So for
rails generate component PageHeader
IWe get a app/component/page_header_component.html.erb and app/component/page_header_component.rb which I edited to be
<header class="relative mb-1 bg-(--color-nav-background) pt-4 pb-4 pl-12 text-center sm:pl-4">
<%= render PageHeaderButtonComponent.new %>
<h1 class="mb-2 text-center text-3xl font-bold text-(--color-title)">
<%= @title %>
</h1>
<% if @subtitle.present? %>
<p class="mb-4 text-center text-[1.2rem] font-normal text-(--color-subtitle)">
<%= @subtitle %>
</p>
<% end %>
<%= render PageHeaderNavComponent.new %>
</header>
And
class PageHeaderComponent < ViewComponent::Base
def initialize(title:, subtitle: nil)
@title = title
@subtitle = subtitle
end
end
But we also get a
# frozen_string_literal: true
require "test_helper"
class PageHeaderHeaderComponentTest < ViewComponent::TestCase
def test_component_renders_something_useful
# assert_equal(
# %(<span>Hello, components!</span>),
# render_inline(PageHeaderHeaderComponent.new(message: "Hello, components!")).css("span").to_html
# )
end
end
This I adapted to be
# frozen_string_literal: true
require "test_helper"
class PageHeaderComponentTest < ViewComponent::TestCase
def test_component_renders_something_useful
rendered = render_inline(PageHeaderComponent.new(title: "my title", subtitle: "my subtitle"))
assert_equal "my title", rendered.css("h1").text.strip
assert_equal "my subtitle", rendered.css("p").text.strip
assert rendered.at_css("button[data-controller='theme-toggle']").present?
end
end
I found I had not done the following
- Defined view_components correctly in the gemfile as it needs test
- Not modified test_helper.rb
So for the gemfile we needed
group :development, :test do
# view_component for building reusable view components [https://viewcomponent.org/]
gem "view_component"
end
And for the test_helper.rb, I am assuming this is autoloading, I need to have
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require "view_component/test_case"
module ActiveSupport
class TestCase
# Run tests in parallel with specified workers
parallelize(workers: :number_of_processors)
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here...
end
end
Sorbet
There is no type safety in ruby but if you use sorbet it maybe prove to be useful. I set this up using mostly using the youtube Type Checking with the Sorbet Gem in Rails 7 | Ruby on Rails 7 Gem Tutorial by deanin. First of all make sure you have the vs code sorbet extension. Next the gemfile
# Runtime for sorbet type checking [https://sorbet.org/]
gem "sorbet", group: :development
gem "sorbet-runtime"
gem "tapioca", require: false, group: [ :development, :test ]
Then run this - said it took a while but not for me.
bundle exec tapioca init
Now run
bin/tapioca dsl
Now we are free to put our types in. So I started with something small. Basically you need the magic line # typed: true and to add extend T::Sig. From there you put in your sig line appropriately
# typed: true
class PageHeaderComponent < ViewComponent::Base
extend T::Sig
sig { params(title: String, subtitle: T.nilable(String)).void }
def initialize(title:, subtitle: nil)
@title = title
@subtitle = subtitle
end
end
Nothing has been easy with this but probably because I am new to this again. A lot of setup I feel. I needed to restart the workspace to get the test cases to appear in test explorer and found the error "The super class `ViewComponent::TestCase` of `PageHeaderComponentTest` does not derive from `Class`" in all of the the tests. The robot fixed it by changing todo.rbi which appears to be an export file for sorbet. It changed
module ViewComponent::TestCase
to
class ViewComponent::TestCase
And the error went away. However I noticed at the top of the file it said
# DO NOT EDIT MANUALLY
# This is an autogenerated file for unresolved constants.
# Please instead update this file by running `bin/tapioca todo`.
# typed: false
So wonder whether this was a plan stand and of course the answer was no. It then fixed it, hopefully properly, by adding the file sorbet/rbi/manual
# typed: true
# Manual stub for ViewComponent test support that isn't auto-discovered by Tapioca
class ViewComponent::TestCase
end
Second Go
Sorbet
Generating rbi
Well the Sorbet stuff was difficult to get going mainly because I used the robots. The finally solution was not to use bin/tapioca dsl but instead
bundle exec tapioca gem
This fixed all of the errors with sorbet but ci.rbi and my test cases
To fix ci.rbi I used a manual rbi in sorbet/rbi/manual/ci.rbi
# typed: true
module ::CI
sig { params(block: T.proc.void).void }
def self.run(&block); end
end
Aliases
In the example for PageDataLoad.rb we have a function which returns json like structure.
sig { params(page_name: String).returns(T::Hash[String, T.any(T::Boolean, String, Integer, Float, NilClass, T::Array[T.untyped], T::Hash[String, T.untyped])]) }
def self.load(page_name)
new(page_name).load
end
You can make aliases for a type which you can define in sorbet/types/json.rb
# typed: strict
# This replaces T::Hash[String, T.untyped] with Types::JSONHash
module Types
JSONValue = T.type_alias { T.any(
String,
Integer,
Float,
TrueClass,
FalseClass,
NilClass,
T::Array[T.untyped],
T::Hash[String, T.untyped]
) }
JSONHash = T.type_alias { T::Hash[String, JSONValue] }
end
Then you need to make sure it gets loaded by adding a types initializer in config/initializers/types.rb
require_relative "../../sorbet/types/json"
Test Case
For the test cases I change them from the above to use Template
# frozen_string_literal: true
# typed: false
require "test_helper"
class PageHeaderComponentTest < ViewComponent::Template
def test_component_renders_something_useful
rendered = render_inline(PageHeaderComponent.new(title: "my title", subtitle: "my subtitle"))
assert_equal "my title", rendered.css("h1").text.strip
assert_equal "my subtitle", rendered.css("p").text.strip
assert rendered.at_css("button[data-controller='theme-toggle']").present?
end
end
Now I can run srb tc with no errors. It even said great job which, for once I agree with.
New Stuff
Some of this was probably not around the last time I look at this. So here is some stuff I have found
Accessors
You can use attr_reader (readonly), attr_writer(writeonly) and attr_accessor(both) to create get/settters. This is convenient to reduce the amount of typing. For instance.
# typed: true
class PageDataError < StandardError
extend T::Sig
attr_reader :status, :detail
sig { params(status: Integer, detail: String).void }
def initialize(status:, detail:)
@status = status
@detail = detail
super(detail)
end
end
Introspection Tools (How to inspect objects, classes, and capabilities)
Ruby gives you a small set of powerful introspection methods. These let you ask objects questions about:
what they are
what they can do
what they contain
how they relate to other classes
These are the core tools you’ll use everywhere.
is_a?
Checks whether an object is an instance of a class or any subclass of that class. Use this when class identity matters (e.g., HTTP response types).
res = Net::HTTP.get_response(@url)
Is this a 2xx success response?
return nil unless res.is_a?(Net::HTTPSuccess)
instance_of?
Checks whether an object is exactly an instance of a class (no subclasses allowed).
1.instance_of?(Integer) # true
1.is_a?(Numeric) # true
1.instance_of?(Numeric) # false (Integer < Numeric)
Use this when you need strict type matching.
respond_to?
Checks whether an object supports a given method. This is Ruby’s duck typing check: “Can it quack?”
user = { name: "Iain" }
if user.respond_to?(:to_json)
puts user.to_json
end
Use this when behavior matters, not class.
nil?
Checks whether a value is literally nil. Ruby has exactly one null value: nil.
value = maybe_get_value
if value.nil?
puts "No value returned"
end
Use this for null checks.
empty?
Checks whether a collection or string is empty.
[].empty? # true
"".empty? # true
[1, 2].empty? # false
Use this when you want to distinguish between “no value” (nil) and “empty value” ("", [], {}).
class
Returns the object’s class.
res = Net::HTTP.get_response(@url)
puts res.class
=> Net::HTTPSuccess
Useful for debugging and exploration.
ancestors
Shows the inheritance chain and included modules. This is how you discover mixins.
Net::HTTPSuccess.ancestors
=> [Net::HTTPSuccess, Net::HTTPResponse, ... , Kernel, BasicObject]
Use this to understand where methods come from.
methods
Lists all methods an object responds to.
"hello".methods.sort
Useful when exploring unfamiliar objects.
instance_variables
Lists all instance variables on an object.
class User
def initialize
@name = "Iain"
@role = "Engineer"
end
end
u = User.new
u.instance_variables
=> [:@name, :@role]
Great for debugging internal state.
defined?
Checks whether something exists without raising an error.
defined?(some_undefined_variable) # => nil
defined?(puts) # => "method"
Use this when probing safely.
kind_of?
Alias for is_a?. Same behavior, different name.
1.kind_of?(Numeric) # true
Debugging
So clearing outputting values is quite important so here is an example to crib from. Pretty simple
class ContentHeaderVideoBannerBlockComponent < ViewComponent::Base
def initialize(header_video_banner_block:)
@block = header_video_banner_block
# Print the Intrinsic Dimensions to the logs for debugging purposes
Rails.logger.debug "Intrinsic Width is #{@block[:intrinsicWidth].inspect}"
Rails.logger.debug "Intrinsic Height is #{@block[:intrinsicHeight].inspect}"
# Approach 2
width = @block[:intrinsicWidth]
Rails.logger.debug "Intrinsic Width is #{width.nil? ? 'nil' : width}"
And we can print a whole class with
Rails.logger.debug do
{
component: self.class.name,
video_name: video_set[:videoName],
image_name: image_set[:imageName],
enabled_video_formats: enabled_video_formats,
enabled_image_formats: enabled_image_formats,
video_sources: @video_sources,
image_sources: @image_sources,
fallback_image_url: @fallback_image_url,
aspect_ratio: @aspect_ratio
}.inspect
end
Little bit on Dry::Schema
Dry is the Zod of Ruby I guess. So not sure if I did this last time but battling my way through it this time around. A simple schema may be:
UserSchema = Dry::Schema.Params do
required(:name).filled(:string)
required(:age).filled(:integer)
optional(:admin).filled(:bool)
end
You can make an instance with:
input = {
"name" => "Iain",
"age" => "42",
"admin" => "true"
}
result = UserSchema.call(input)
If you inspect it, it will look like this:
#<Dry::Schema::Result{:name=>"Iain", :age=>42, :admin=>true} errors={} >
Using `to_h` converts the result into a plain Ruby Hash:
validated = result.to_h
And you will see:
{
name: "Iain",
age: 42,
admin: true
}