Profiling 📊 Ruby on Rails 8 Applications: Essential Tools and Techniques

Introduction

Performance optimization is critical for delivering fast, responsive Rails applications. This comprehensive guide covers the most important profiling tools you should implement in your Rails 8 application, complete with setup instructions and practical examples.

Why Profiling Matters

Before diving into tools, let’s understand why profiling is essential:

  1. Identify bottlenecks: Pinpoint exactly which parts of your application are slowing things down
  2. Optimize resource usage: Reduce memory consumption and CPU usage
  3. Improve user experience: Faster response times lead to happier users
  4. Reduce infrastructure costs: Efficient applications require fewer server resources

Essential Profiling Tools for Rails 8

1. Rack MiniProfiler

What it does: Provides real-time profiling of your application’s performance directly in your browser.

Why it’s important: It’s the quickest way to see performance metrics without leaving your development environment.

Installation:

# Gemfile
gem 'rack-mini-profiler', group: :development

Usage example:
After installation, it automatically appears in your browser’s corner showing:

  • SQL query times
  • Ruby execution time
  • Memory allocation
  • Flamegraphs (with additional setup)

Advantages:

  • No configuration needed for basic setup
  • Shows N+1 query warnings
  • Integrates with Rails out of the box

GitHubhttps://github.com/MiniProfiler/rack-mini-profiler
Documentationhttps://miniprofiler.com/

2. Bullet

What it does: Detects N+1 queries, unused eager loading, and missing counter caches.

Why it’s important: N+1 queries are among the most common performance issues in Rails applications.

Installation:

# Gemfile
gem 'bullet', group: :development

Configuration:

# config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
end

Example output:

GET /posts
USE eager loading detected
  Post => [:comments]
  Add to your query: Post.includes([:comments])

Advantages:

  • Catches common ORM performance issues early
  • Provides specific recommendations for fixes
  • Works across all environments

GitHubhttps://github.com/flyerhzm/bullet
Documentationhttps://github.com/flyerhzm/bullet/blob/master/README.md

3. Ruby Prof (and StackProf)

What it does: Low-level Ruby code profiler that shows exactly where time is being spent.

Why it’s important: When you need deep insight into method-level performance characteristics.

Installation:

# Gemfile
gem 'ruby-prof', group: :development
gem 'stackprof', group: :development

Usage example:

# In your controller or service object
result = RubyProf.profile do
  # Code you want to profile
end

printer = RubyProf::GraphPrinter.new(result)
printer.print(STDOUT, {})

For StackProf:

StackProf.run(mode: :cpu, out: 'tmp/stackprof.dump') do
  # Code to profile
end

Advantages:

  • Method-level granularity
  • Multiple output formats (call graphs, flamegraphs)
  • StackProf is sampling-based so has lower overhead

GitHubhttps://github.com/ruby-prof/ruby-prof
Documentationhttps://github.com/ruby-prof/ruby-prof/blob/master/README.md

StackProf Alternative:
GitHubhttps://github.com/tmm1/stackprof
Documentationhttps://github.com/tmm1/stackprof/blob/master/README.md

4. Memory Profiler

What it does: Tracks memory allocations and helps identify memory bloat.

Why it’s important: Memory issues can lead to slow performance and even crashes.

Installation:

# Gemfile
gem 'memory_profiler', group: :development

Usage example:

report = MemoryProfiler.report do
  # Code to profile
end

report.pretty_print(to_file: 'memory_report.txt')

Advantages:

  • Shows allocated objects by class and location
  • Tracks retained memory after GC
  • Helps find memory leaks

GitHubhttps://github.com/SamSaffron/memory_profiler
Documentationhttps://github.com/SamSaffron/memory_profiler/blob/master/README.md

5. Skylight

What it does: Production-grade application performance monitoring (APM).

Why it’s important: Understanding real-world performance characteristics is different from development profiling.

Installation:

# Gemfile
gem 'skylight'

Configuration:

# config/skylight.yml
production:
  authentication: [YOUR_AUTH_TOKEN]

Advantages:

  • Low-overhead production profiling
  • Endpoint-level performance breakdowns
  • Database query analysis
  • Exception tracking

Websitehttps://www.skylight.io
Documentationhttps://docs.skylight.io
GitHubhttps://github.com/skylightio/skylight-ruby

6. AppSignal

What it does: Full-stack performance monitoring and error tracking.

Why it’s important: Provides comprehensive insights across your entire application stack.

Installation:

# Gemfile
gem 'appsignal'

Then run:

bundle exec appsignal install YOUR_PUSH_API_KEY

Advantages:

  • Error tracking alongside performance
  • Host metrics integration
  • Background job monitoring
  • Magic Dashboard for quick insights

Websitehttps://appsignal.com
Documentationhttps://docs.appsignal.com/ruby
GitHubhttps://github.com/appsignal/appsignal-ruby

7. Derailed Benchmarks

What it does: Suite of benchmarks and performance tests for your application.

Why it’s important: Helps catch performance regressions before they hit production.

Installation:

# Gemfile
group :development, :test do
  gem 'derailed_benchmarks'
end

Usage examples:

# Memory usage at boot
bundle exec derailed bundle:mem

# Performance per route
bundle exec derailed exec perf:routes

Advantages:

  • CI-friendly performance testing
  • Memory usage analysis
  • Route-based performance testing

GitHubhttps://github.com/schneems/derailed_benchmarks
Documentationhttps://github.com/schneems/derailed_benchmarks/blob/master/README.md

8. Flamegraph Generation

What it does: Visual representation of where time is being spent in your application.

Why it’s important: Provides an intuitive way to understand call stacks and hot paths.

Installation:

# Gemfile
gem 'flamegraph'
gem 'stackprof' # if not already installed

Usage example:

Flamegraph.generate('flamegraph.html') do
  # Code to profile
end

Advantages:

  • Visual representation of performance
  • Easy to spot hot paths
  • Interactive exploration

GitHubhttps://github.com/SamSaffron/flamegraph
Documentationhttp://samsaffron.github.io/flamegraph/rails-startup.html

Additional Helpful Tools 🔧

9. Benchmark-ips

Benchmark-ips (iterations per second) is a superior benchmarking tool compared to Ruby’s standard Benchmark library. It provides:

  1. Iterations-per-second measurement – More intuitive than raw time measurements
  2. Statistical analysis – Shows standard deviation between runs
  3. Comparison mode – Easily compare different implementations
  4. Warmup phase – Accounts for JIT and cache warming effects

Benchmark-ips solves these problems and is particularly valuable for:

  • Comparing algorithm implementations
  • Testing performance optimizations
  • Benchmarking gem alternatives
  • Validating performance-critical code

GitHubhttps://github.com/evanphx/benchmark-ips
Documentationhttps://github.com/evanphx/benchmark-ips/blob/master/README.md

Installation
# Gemfile
gem 'benchmark-ips', group: :development
Basic Usage:
require 'benchmark/ips'

Benchmark.ips do |x|
  x.report("addition") { 1 + 2 }
  x.report("addition with to_s") { (1 + 2).to_s }
  x.compare!
end
Advanced Features:
Benchmark.ips do |x|
  x.time = 5 # Run each benchmark for 5 seconds
  x.warmup = 2 # Warmup time of 2 seconds
  
  x.report("Array#each") { [1,2,3].each { |i| i * i } }
  x.report("Array#map") { [1,2,3].map { |i| i * i } }
  
  # Add custom statistics
  x.config(stats: :bootstrap, confidence: 95)
  
  x.compare!
end
# Memory measurement
require 'benchmark/memory'

Benchmark.memory do |x|
  x.report("method1") { ... }
  x.report("method2") { ... }
  x.compare!
end

# Disable GC for more consistent results
Benchmark.ips do |x|
  x.config(time: 5, warmup: 2, suite: GCSuite.new)
end
Sample Output:
Warming up --------------------------------------
            addition    281.899k i/100ms
  addition with to_s    261.831k i/100ms
Calculating -------------------------------------
            addition      8.614M (± 1.2%) i/s -     43.214M in   5.015800s
  addition with to_s      7.017M (± 1.8%) i/s -     35.347M in   5.038446s

Comparison:
            addition:  8613594.0 i/s
  addition with to_s:  7016953.3 i/s - 1.23x slower

Key Advantages

  1. Accurate comparisons with statistical significance
  2. Warmup phase eliminates JIT/caching distortions
  3. Memory measurements available through extensions
  4. Customizable reporting with various statistics options

10. Rails Performance (Dashboard)

What is Rails Performance?

Rails Performance is a self-hosted alternative to New Relic/Skylight that provides:

  1. Request performance tracking
  2. Background job monitoring
  3. Slowest endpoints identification
  4. Error tracking
  5. Custom event monitoring
Why It’s Important

For teams that:

  • Can’t use commercial SaaS solutions
  • Need to keep performance data in-house
  • Want historical performance tracking
  • Need simple setup without complex infrastructure

GitHubhttps://github.com/igorkasyanchuk/rails_performance
Documentationhttps://github.com/igorkasyanchuk/rails_performance/blob/master/README.md

Installation
# Gemfile
gem 'rails_performance', group: :development

Then run:

rails g rails_performance:install
rake db:migrate
Configuration
# config/initializers/rails_performance.rb
RailsPerformance.setup do |config|
  config.redis = Redis.new # optional, will use Rails.cache otherwise
  config.duration = 4.hours # store requests for 4 hours
  config.enabled = Rails.env.production?
  config.http_basic_authentication_enabled = true
  config.http_basic_authentication_user_name = 'admin'
  config.http_basic_authentication_password = 'password'
end
Accessing the Dashboard:

After installation, access the dashboard at:

http://localhost:3000/rails/performance

Custom Tracking:

# Track custom events
RailsPerformance.trace("custom_event", tags: { type: "import" }) do
  # Your code here
end

# Track background jobs
class MyJob < ApplicationJob
  around_perform do |job, block|
    RailsPerformance.trace(job.class.name, tags: job.arguments) do
      block.call
    end
  end
end
# Add custom fields to requests
RailsPerformance.attach_extra_payload do |payload|
  payload[:user_id] = current_user.id if current_user
end

# Track slow queries
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
  if event.duration > 100 # ms
    RailsPerformance.trace("slow_query", payload: {
      sql: event.payload[:sql],
      duration: event.duration
    })
  end
end
Sample Dashboard Views:
  1. Requests Overview:
    • Average response time
    • Requests per minute
    • Slowest actions
  2. Detailed Request View:
    • SQL queries breakdown
    • View rendering time
    • Memory allocation
  3. Background Jobs:
    • Job execution time
    • Failures
    • Queue times
Key Advantages
  1. Self-hosted solution – No data leaves your infrastructure
  2. Simple setup – No complex dependencies
  3. Historical data – Track performance over time
  4. Custom events – Track any application events
  5. Background jobs – Full visibility into async processes

Implementing a Complete Profiling Strategy

For a comprehensive approach, combine these tools at different stages:

  1. Development:
  • Rack MiniProfiler (always on)
  • Bullet (catch N+1s early)
  • RubyProf/StackProf (for deep dives)
  1. CI Pipeline:
  • Derailed Benchmarks
  • Memory tests
  1. Production:
  • Skylight or AppSignal
  • Error tracking with performance context

Sample Rails 8 Configuration

Here’s how to set up a complete profiling environment in a new Rails 8 app:

# Gemfile

# Development profiling
group :development do
  # Basic profiling
  gem 'rack-mini-profiler'
  gem 'bullet'
  
  # Deep profiling
  gem 'ruby-prof'
  gem 'stackprof'
  gem 'memory_profiler'
  gem 'flamegraph'
  
  # Benchmarking
  gem 'derailed_benchmarks', require: false
  gem 'benchmark-ips'
  
  # Dashboard
  gem 'rails_performance'
end

# Production monitoring (choose one)
group :production do
  gem 'skylight'
  # or
  gem 'appsignal'
  # or
  gem 'newrelic_rpm' # Alternative option
end

Then create an initializer for development profiling:

# config/initializers/profiling.rb
if Rails.env.development?
  require 'rack-mini-profiler'
  Rack::MiniProfilerRails.initialize!(Rails.application)

  Rails.application.config.after_initialize do
    Bullet.enable = true
    Bullet.alert = true
    Bullet.bullet_logger = true
    Bullet.rails_logger = true
  end
end

Conclusion

Profiling your Rails 8 application shouldn’t be an afterthought. By implementing these tools throughout your development lifecycle, you’ll catch performance issues early, maintain a fast application, and provide better user experiences.

Remember:

  • Use development tools like MiniProfiler and Bullet daily
  • Run deeper profiles with RubyProf before optimization work
  • Monitor production with Skylight or AppSignal
  • Establish performance benchmarks with Derailed

With this toolkit, you’ll be well-equipped to build and maintain high-performance Rails 8 applications.

Enjoy Rails! 🚀

Setup 🛠 Rails 8 App – Part 8: Debugbar – Apply performance 📈 optimization

1. Integrate pagy for pagination

Why it’s the great choice:

  • Super fast and lightweight (~300x faster than Kaminari or WillPaginate).
  • No dependencies on Active Record or view helpers.
  • Very customizable and modular (can do Bootstrap/Tailwind/semantic UI integrations).
  • Supports metadata, responsive pagination, overflow handling, infinite scrolling, and JSON API pagination.
# Gemfile
# The Best Pagination Ruby Gem [https://ddnexus.github.io/pagy/]
gem "pagy", "~> 9.3" # omit patch digit

bundle install
Example Usage in Controller:
include Pagy::Backend

def index
  @pagy, @products = pagy(Product.all)
end

In Product Helper / Application Helper:
include Pagy::Frontend
In the View (ERB or HAML):
<%= pagy_nav(@pagy) %>
Add an initializer file

Download the file from: https://ddnexus.github.io/pagy/quick-start/

https://ddnexus.github.io/pagy/gem/config/pagy.rb

and save it into the config/initializers directory. Uncomment limit and size options.

Tailwind Support:
# In an initializer (e.g., config/initializers/pagy.rb)
Pagy::DEFAULT[:limit]       = 20                    # default
Pagy::DEFAULT[:size]        = 7                     # default
# Better user experience handled automatically
require "pagy/extras/overflow"
Pagy::DEFAULT[:overflow] = :last_page

I am getting a load error when I want tailwind css to apply to my views:

LoadError: cannot load such file -- pagy/extras/tailwind (LoadError)

Ahh it’s not supporting Tailwind CSS, and there is no tailwind file found in the Gem too!

Hmm..😟 Check below:

We can try to include the css manually, check: https://ddnexus.github.io/pagy/docs/api/stylesheets/#pagy-tailwind-css

Create a file pagy.tailwind.css and add the following:

.pagy {
    @apply flex space-x-1 font-semibold text-sm text-gray-500;
    a:not(.gap) {
      @apply block rounded-lg px-3 py-1 bg-gray-200;
      &:hover {
        @apply bg-gray-300;
      }
      &:not([href]) { /* disabled links */
        @apply text-gray-300 bg-gray-100 cursor-default;
      }
      &.current {
        @apply text-white bg-gray-400;
      }
    }
    label {
      @apply inline-block whitespace-nowrap bg-gray-200 rounded-lg px-3 py-0.5;
      input {
        @apply bg-gray-100 border-none rounded-md;
      }
    }
  }

Modify app/assets/tailwind/application.css :

@import "tailwindcss";
@import "./pagy.tailwind.css";

Restart your server and you got it!

Testing performance

You can see that in the query Tab in Debugbar, select * from products query has been replaced with limit query. But this is not the case where you go through the entire thousand hundreds of products, for example searching. We can think of view caching and SQL indexing for such a situation.

to be continued.. 🚀

Setup 🛠 Rails 8 App – Part 7: Mastering Debugbar 👾 for Rails Performance Optimization

As Rails developers, we’ve all been there – your application starts slowing down as data grows, pages take longer to load, and memory usage spikes. Before you blame Rails itself or consider rewriting your entire application, you should profile your app to understand what’s really happening behind the scenes.

Most of the time, the issue lies in how the app is written: unnecessary SQL queries, excessive object allocations, or inefficient code patterns. Before you think about rewriting your app or switching frameworks, profile it.

That’s where Rails Debugbar shines— It helps you identify bottlenecks like slow database queries, excessive object allocations, and memory leaks – all from a convenient toolbar at the bottom of your development environment.


🤔 What is Rails Debugbar?

Rails Debugbar is a browser-integrated dev tool that adds a neat, powerful panel at the bottom of your app in development. It helps you answer questions like:

  • How long is a request taking?
  • How many SQL queries are being executed?
  • How many Ruby objects are being allocated?
  • Which parts of my code are slow?

It’s like a surgeon’s X-ray for your app—giving you visibility into internals without needing to dig into logs or guess. Get a better understanding of your application performance and behavior (SQL queries, jobs, cache, routes, logs, etc)


⚙️ Installation & Setup (Rails 8)

Prerequisites

  • Ruby on Rails 5.2+ (works perfectly with Rails 8)
  • A Ruby version supported by your Rails version

1. Add it to your Gemfile:

group :development do
  gem 'debugbar'
end

Then run:

bundle install

2. Add the Debugbar layout helpers in your application layout:

In app/views/layouts/application.html.erb, just before the closing </head> and </body> tags:

<%= debugbar_head if defined?(Debugbar) %>
...
<%= debugbar_body if defined?(Debugbar) %>

That’s it! When you restart your server, you’ll see a sleek Debugbar docked at the bottom of the screen.

You can see ActionCable interacting with debugbar_channel in logs:

[ActionCable] Broadcasting to debugbar_channel: [{id: "xxxx-xxxx-xxxx-xxxx", meta: {controller: "ProductsController", action: "show", params: {"controller" => "products", "action" => "show", "id" => "3"}, format: :html, method: "GET", path: "/products/3", status: 200, view_runtime: 10.606000004219823, db_runtime: 0.44599999819...

23:47:17 web.1  | Debugbar::DebugbarChannel transmitting [{"id" => "xxxx-xxxx-xxxx-xxxx", "meta" => {"controller" => "ProductsController", "action" => "show", "params" => {"controller" => "products", "action" => "show", "id" => "3"}, "format" => "html", "method" => "GET", "path" => "/products/3", "status" => 200, "view_runtime" => 10.6... (via streamed from debugbar_channel)

23:47:17 web.1  | Debugbar::DebugbarChannel#receive({"ids" => ["xxxx-xxxx-xxxx-xxxx"]})
23:47:17 web.1  | [ActionCable] Broadcasting to debugbar_channel: []

23:47:17 web.1  | Debugbar::DebugbarChannel transmitting [] (via streamed from debugbar_channel)

📚 Official links for reference:


🔍 Exploring the Debugbar Tabs

Rails Debugbar includes several tabs. Let’s go through the most useful ones—with real-world examples of how to interpret and improve performance using the data.

1. Queries Tab

This tab shows all SQL queries executed during the request, including their duration in milliseconds.

Example:

You see this in the Queries tab:

SELECT * FROM users WHERE email = 'test@example.com'  (15ms)
SELECT * FROM products WHERE user_id = 1                 (20ms)
SELECT * FROM comments WHERE product_id IN (...)         (150ms)

You realize:

  • The third query is taking 10x more time.
  • You’re not using eager loading, and it’s triggering N+1 queries.

How to Fix:

Update your controller:

@products = Product.includes(:comments).where(user_id: 1)

This loads the comments in a single query, reducing load time and object allocation.


2. Timeline Tab

Gives you a timeline breakdown of how long each part of the request takes—view rendering, database, middleware, etc.

Example:

You notice that rendering a partial takes 120ms, way more than expected.

<%= render 'shared/sidebar' %>

How to Fix:

Check the partial for:

  • Heavy loops or database calls
  • Uncached helper methods

Move the partial to use a fragment cache:

<% cache('sidebar') do %>
  <%= render 'shared/sidebar' %>
<% end %>

Another Example Problem:
If you notice view rendering takes 800ms for a simple page.

Solution:
Investigate partials being rendered. You might be:

  • Rendering unnecessary partials
  • Using complex helpers in views
  • Need to implement caching
# Before
<%= render @products %> # Renders _product.html.erb for each

# After (with caching)
<% @products.each do |product| %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

3. Memory Tab

Tracks memory usage and object allocations per request.

Example:

You load a dashboard page and see 25,000+ objects allocated. Yikes.

Dig into the view and see:

<% User.all.each do |user| %>
  ...
<% end %>

That’s loading all users into memory.

How to Fix:

Use pagination or lazy loading:

@users = User.page(params[:page]).per(20)

Now the object count drops dramatically.


4. Environment & Request Info

See request parameters, environment variables, session data, and headers.

Example:

You’re debugging an API endpoint and want to confirm the incoming headers or params—Debugbar shows them neatly in this tab.

It can help identify:

  • Wrong content-type headers
  • CSRF issues
  • Auth headers or missing cookies

💡 Debugbar Best Practices

  • Use it early: Don’t wait until your app is slow—profile as you build.
  • Watch out for hidden N+1 in associations, partials, or background jobs.
  • Keep an eye on object counts to reduce memory pressure in production.
  • Use fragment and Russian doll caching where needed, based on render timelines.
  • Regularly review slow pages with Debugbar open—it’s a development-time lifesaver.

💭 Final Thoughts

Rails Debugbar offers an easy, visual way to profile and optimize your Rails 8 app. Whether you’re debugging a slow page, inspecting a query storm, or chasing down memory leaks, this tool gives you insight without friction.

So before you overhaul your architecture or blame Rails, fire up Debugbar—and fix the real issues.

to be modified..  🚀

Inside Rails: The Role of Rack 🗄 and Middleware 🔌

Rack provides a minimal, modular, and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the bridge between web servers, web frameworks, and web application into a single method call.

Where is it used?

  • Rails (built on Rack)
  • Sinatra and Hanami
  • Middleware development

What is a Rack-Based Application?

A Rack-based application is any Ruby web application that implements the Rack interface. This means the app must follow Rack’s simple calling convention:

app = Proc.new do |env|
  ['200', { 'Content-Type' => 'text/html' }, ['Hello, Rack!']]
end

This returns an array of three elements:

  1. HTTP status code ('200')
  2. Headers ({ 'Content-Type' => 'text/html' })
  3. Response body (['Hello, Rack!'])
Example: Basic Rack Application
require 'rack'

app = Proc.new do |env|
  ['200', { 'Content-Type' => 'text/html' }, ['Hello, Rack!']]
end

Rack::Handler::WEBrick.run app, Port: 9292

Run it with:

ruby my_rack_app.rb

Open http://localhost:9292 in your browser.

Does Rails Use Rack?

Yes, Rails uses Rack. Rack serves as the interface between Rails and web servers like Puma or WEBrick.

How Rails Uses Rack

When a request comes in:

  1. The web server (Puma/WEBrick) receives it.
  2. The server passes the request to Rack.
  3. Rack processes the request and sends it through Rails middleware.
  4. After passing through the middleware stack, Rails’ router (ActionDispatch) decides which controller/action should handle the request.
  5. The response is generated, sent back through Rack, and returned to the web server.

Check /design_studio/config.ru file in our Rails 8 app is responsible for starting the server.

You can actually run a Rails app using just Rack!

  1. Create a config.ru file / use existing one:
require_relative 'config/environment'
run Rails.application
  1. Run it using Rack:
rackup -p 4343

open http://localhost:4343/products

This runs your Rails app without Puma or WEBrick, proving Rails works via Rack.

Is Rack a Server?

No, Rack is not a server. Instead, Rack is a middleware interface that sits between the web server (like Puma or WEBrick) and your Ruby application (like Rails or Sinatra).

How Does Rack Fit with Web Servers Like Puma and WEBrick?

Puma and WEBrick support Rack by implementing the Rack::Handler interface, allowing them to serve any Rack-based application, such as Rails and Sinatra.

  • Puma and WEBrick are not built “on top of” Rack—they are independent web servers.
  • However, they implement Rack::Handler, which means they support Rack applications.
  • This allows them to serve Rails, Sinatra, and other Rack-based applications.

The Relationship Between Rack, Web Servers, and Rails

  1. Rack provides a standard API for handling HTTP requests and responses.
  2. Web servers (Puma, WEBrick, etc.) implement Rack::Handler so they can run any Rack-based app.
  3. Rails supports Rack by implementing the Rack interface, allowing it to interact with web servers and middleware.

How Rails Supports Rack

  1. Rack Middleware: Rails includes middleware components that process requests before they reach controllers.
  2. Rack Interface: Rails applications can be run using config.ru, which follows the Rack convention.
  3. Web Server Communication: Rails works with Rack-compatible servers like Puma and WEBrick.

Illustration of How a Request Flows

  1. The browser sends a request to the server (Puma/WEBrick).
  2. The server passes the request to Rack.
  3. Rack processes the request (passing it through middleware).
  4. Rails handles the request and generates a response.
  5. The response goes back through Rack and is sent to the server, which then passes it to the browser.

So, while Rack is not a server, it allows web servers to communicate with Ruby web applications like Rails.

Adding Middleware in a Rails 8 App

Middleware is a way to process requests before they reach your Rails application.

How Does Middleware Fit In?

Middleware in Rails is just a Rack application that modifies requests/responses before they reach the main Rails app.

Example: Custom Middleware

Create a new file in app/middleware/my_middleware.rb:

class MyMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    body = ["Custom Middleware: "] + body
    [status, headers, body]
  end
end

Now, add it to Rails in config/application.rb:

config.middleware.use MyMiddleware

Restart your Rails server, and all responses will be prefixed with Custom Middleware:

Understanding Confusing 🧐 Ruby Concepts: Procfile, Rake, Rack, and More

Ruby has several terms that sound similar but serve different purposes. If you’ve ever been confused by things like Procfile, Rakefile, Rack, and Rake, this guide will clarify them all. Plus, we’ll cover additional tricky concepts you might have overlooked!

1. Procfile

What is it?

A Procfile is a text file used in deployment environments (like Heroku and Kamal) to specify how your application should be started.

Where is it used?

Platforms like Heroku, Kamal, and Foreman use Procfile to define process types (like web servers and workers).

Example:

web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq

  • web: Starts the Puma web server.
  • worker: Runs background jobs using Sidekiq.

Check the post for details (Foreman): https://railsdrop.com/2025/03/26/setup-rails-8-app-part-4-tailwind-css-into-the-action/

2. Rake and Rakefile

What is Rake?

Rake is a task management tool for automating scripts in Ruby applications. It’s like Makefile but written in Ruby.

What is a Rakefile?

A Rakefile is a Ruby file where Rake tasks are defined.

Check Rails::Railtie.rake_tasks for more info.

Where is it used?

  • Rails applications (for tasks like database migrations and data seeding)
  • Standalone Ruby applications (for automating scripts)

Common Rake Commands in Rails:

rake db:migrate    # Run database migrations
rake db:seed       # Seed the database
rake routes        # Show all available routes

Example Custom Rake Task:

Create a file at lib/tasks/custom.rake:

namespace :custom do
  desc "Prints a greeting"
  task hello: :environment do
    puts "Hello from custom Rake task!"
  end
end

Run it with:

rake custom:hello

3. RackWhat is it?

Rack is a lightweight interface between Ruby web applications and web servers. It provides a simple way to handle HTTP requests and responses.

https://github.com/rack/rack

Checkout Rack in more detail: https://railsdrop.com/2025/04/07/inside-rails-the-role-of-rack-and-middleware/

4. Adding Middleware in a Rails 8 App

Checkout the post: https://railsdrop.com/2025/04/07/inside-rails-the-role-of-rack-and-middleware/

5. Other Confusing Ruby Concepts You Should Know

Gemfile vs. Gemspec

  • Gemfile: Defines dependencies for a Rails project (uses Bundler).
  • Gemspec: Defines dependencies and metadata for a Ruby gem.

Lambda vs. Proc

Both are used for defining anonymous functions, but behave differently:

lambda_example = -> { return "Lambda returns from itself" }
proc_example = Proc.new { return "Proc returns from the enclosing method" }

Safe Navigation Operator (&.)

user&.profile&.name  # Avoids NoMethodError if user or profile is nil

Symbol vs. String

:my_symbol  # Immutable, faster lookup
"my_string" # Mutable, slower lookup

&: Shortcut for Blocks

Ruby allows a shorthand syntax for passing methods as blocks using &:.

["hello", "world"].map(&:upcase)  # => ["HELLO", "WORLD"]

Equivalent to:

["hello", "world"].map { |word| word.upcase }

Single Splat (*) Operator

The * operator is used for handling variable-length arguments in methods.

def sum(*numbers)
  numbers.reduce(:+)
end

puts sum(1, 2, 3, 4)  # Output: 10

It can also be used for array expansion (spreads out Arrays):

arr = [1, 2, 3, 4]
> puts *arr
1
2
3
4
=> nil

odds = [3, 5, 7, 9]
puts *odds
>
3
5
7
9
=> nil

first_odd, *rest = odds
> puts rest
5
7
9
=> nil

We can also insert array elements into another Array. In the example below, odds elements are added to the numbers Array, starting from the position where *odds is called.

odds = [3, 5, 7, 9]
numbers = [1, 2, *odds, 10]
puts "numbers: #{numbers}"

# =>
# numbers: [1, 2, 3, 5, 7, 9, 10]

Double Splat (**) in Method Arguments

The ** operator is used to capture keyword arguments.

def greet(name:, **options)
  puts "Hello, #{name}!"
  puts "Options: #{options}"
end

greet(name: "Alice", age: 25, city: "New York")
# Output:
# Hello, Alice!
# Options: {:age=>25, :city=>"New York"}

What Are Keyword Arguments (kwargs) in Ruby?

(name:) in greet is an example of a keyword argument (kwargs).

Keyword arguments allow you to pass arguments to a method using explicit parameter names, making the code more readable and flexible.

Example: Using a Required Keyword Argument
def greet(name:)
  puts "Hello, #{name}!"
end

greet(name: "Alice")  # Output: Hello, Alice!
  • The name: argument must be provided, otherwise, Ruby will raise an error.
Example: Using Optional Keyword Arguments

You can provide default values for keyword arguments:

def greet(name: "Guest")
  puts "Hello, #{name}!"
end

greet        # Output: Hello, Guest!
greet(name: "Bob")  # Output: Hello, Bob!
Example: Combining Required and Optional Keyword Arguments
def greet(name:, age: nil)
  puts "Hello, #{name}!"
  puts "You are #{age} years old." if age
end

greet(name: "Alice", age: 25)
# Output:
# Hello, Alice!
# You are 25 years old.
Example: Capturing Extra Keyword Arguments with **options

The ** operator captures any additional keyword arguments passed to the method into a hash.

def greet(name:, **options)
  puts "Hello, #{name}!"
  puts "Additional Info: #{options}"
end

greet(name: "Alice", age: 25, city: "New York")
# Output:
# Hello, Alice!
# Additional Info: {:age=>25, :city=>"New York"}

**options collects { age: 25, city: "New York" } as a hash.

Check: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/

Also check the latest Ruby that is released last week:

Final Thoughts

Ruby has many terms that seem similar but have distinct uses. By understanding Procfile, Rake, Rack, and middleware in Rails 8, you’ll have a much clearer picture of how Ruby applications work under the hood. If you’re working on a Rails 8 app, take some time to explore these concepts further—they’ll definitely make your life easier!

Happy coding! 🚀

Setup 🛠 Rails 8 App – Part 6: Attach images to Product model

To attach multiple images to a Product model in Rails 8, Active Storage provides the best way using has_many_attached. Below are the steps to set up multiple image attachments in a local development environment.


1️⃣ Install Active Storage (if not already installed)

We have already done this step if you are following this series. Else run the following command to generate the necessary database migrations:

rails active_storage:install
rails db:migrate

This will create two tables in your database:

  • active_storage_blobs → Stores metadata of uploaded files.
  • active_storage_attachments → Creates associations between models and uploaded files.

2️⃣ Update the Product Model

Configuring specific variants is done the same way as has_one_attached, by calling the variant method on the yielded attachable object:

add in app/models/product.rb:

class Product < ApplicationRecord
  has_many_attached :images do |attachable|
    attachable.variant :normal, resize_to_limit: [540, 720]
    attachable.variant :thumb, resize_to_limit: [100, 100]
  end
end

You just have to mention the above and rails will create everything for you!

Variants rely on ImageProcessing gem for the actual transformations of the file, so you must add gem "image_processing" to your Gemfile if you wish to use variants.

By default, images will be processed with ImageMagick using the MiniMagick gem, but you can also switch to the libvips processor operated by the ruby-vips gem.

Rails.application.config.active_storage.variant_processor
# => :mini_magick

Rails.application.config.active_storage.variant_processor = :vips
# => :vips

3️⃣ Configure Active Storage for Local Development

By default, Rails stores uploaded files in storage/ under your project directory.

Ensure your config/environments/development.rb has:

config.active_storage.service = :local

And check config/storage.yml to ensure you have:

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

This will store the uploaded files in storage/.


4️⃣ Add File Uploads in Controller

Modify app/controllers/products_controller.rb to allow multiple image uploads:

class ProductsController < ApplicationController
  def create
    @product = Product.new(product_params)

    if @product.save
      redirect_to @product, notice: "Product was successfully created."
    else
      render :new
    end
  end

  private

  def product_params
    params.require(:product).permit(:name, :description, images: [])
  end
end

Notice images: [] → This allows multiple images to be uploaded.


5️⃣ Update Form for Multiple Image Uploads

Modify app/views/products/_form.html.erb:

<%= form_with model: @product, local: true do |form| %>
  <%= form.label :name %>
  <%= form.text_field :name %>

  <%= form.label :description %>
  <%= form.text_area :description %>

  <%= form.label :images %>
  <%= form.file_field :images, multiple: true %>

  <%= form.submit "Create Product" %>
<% end %>

🔹 multiple: true → Allows selecting multiple files.


6️⃣ Display Images in View

Modify app/views/products/_product.html.erb:

<h1><%= product.name %></h1>
<p><%= product.description %></p>

<h3>Product Images:</h3>
<% product.images.each do |image| %>
  <%= image_tag image.variant(:thumb), alt: "Product Image" %>
<% end %>
<% product.images.each do |image| %>
  <%= image_tag image, alt: "Product Image" %>
<% end %>

Replacing vs Adding Attachments

By default in Rails, attaching files to a has_many_attached association will replace any existing attachments.

To keep existing attachments, you can use hidden form fields with the signed_id of each attached file:

<% @message.images.each do |image| %>
  <%= form.hidden_field :images, multiple: true, value: image.signed_id %>
<% end %>

<%= form.file_field :images, multiple: true %>

This has the advantage of making it possible to remove existing attachments selectively, e.g. by using JavaScript to remove individual hidden fields.


7️⃣ Get Image URLs

In Rails Console (rails c):

product = Product.last
product.images.each do |image|
  puts Rails.application.routes.url_helpers.rails_blob_url(image, host: "http://localhost:3000")
end

This generates a direct URL for each attached image.


8️⃣ Delete an Attached Image

To remove an image from a product:

product = Product.last
product.images.first.purge  # Deletes a single image

To remove all images:

product.images.purge_later


Final Thoughts

  • has_many_attached :images is the best approach for multiple image uploads.
  • Local storage (storage/) is great for development, but for production, use S3 or another cloud storage.
  • Variants allow resizing images before displaying them.

Check: https://guides.rubyonrails.org/active_storage_overview.html https://github.com/<username>/<project>/tree/main/app/views/products

Enjoy Rails! 🚀

to be continued..

Setup 🛠 Rails 8 App – Part 5: Active Storage File Uploads 📤

Meanwhile we are setting up some UI for our app using Tailwind CSS, I have uploaded 2 images to our product in the rich text editor. Let’s discuss about this in this post.

Understanding Active Storage in Rails 8: A Deep Dive into Image Uploads

In our Rails 8 application, we recently tested uploading two images to a product using the rich text editor. This process internally triggers several actions within Active Storage. Let’s break down what happens behind the scenes.

How Active Storage Handles Image Uploads

When an image is uploaded, Rails 8 processes it through Active Storage, creating a new blob entry and storing it in the disk service. The following request is fired:

Processing by ActiveStorage::DirectUploadsController#create as JSON

Parameters: {"blob" => {"filename" => "floral-kurtha.jpg", "content_type" => "image/jpeg", "byte_size" => 107508, "checksum" => "GgNgNxxxxxxxjdPOLw=="}}

This request initiates a database entry in active_storage_blobs:

INSERT INTO "active_storage_blobs" ("key", "filename", "content_type", "service_name", "byte_size", "checksum", "created_at")
VALUES ('huk9dxxxxxxxx09e2cyiq', 'floral-kurtha.jpg', 'image/jpeg', 'local', 107312, 'Fxxxxxxd+bpRibo2EfvA==', '2025-03-31 08:10:07.232453')

Storing Files and Generating URLs

Once the blob entry is created, Rails stores the file on disk and generates a URL:

http://localhost:3000/rails/active_storage/disk/eyJfcmFpbHMiOnsiYSI6eyJxxxxxxx

This process triggers the ActiveStorage::DiskController, handling file storage via a PUT request:

Started PUT "/rails/active_storage/disk/eyJfcmFpbHMiOxxxxx"
Disk Storage (0.9ms) Uploaded file to key: hut9d0zxssxxxxxx
Completed 204 No Content in 96ms

Retrieving Images from Active Storage

After successfully storing the file, the application fetches the image via a GET request:

Started GET "/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOxxxxxxxxxxfQ==--f9c556012577xxxxxxxxxxxxfa21/floral-kurtha-2.jpg"

This request is handled by:

Processing by ActiveStorage::Blobs::RedirectController#show as JPEG

The file is then served via the ActiveStorage::DiskController#show:

Redirected to http://localhost:3000/rails/active_storage/disk/eyJfcmFpbHMiOnsiZGxxxxxxxxxd048aae4ab5c30/floral-kurtha-2.jpg

Updating Records with Active Storage Attachments

When updating a product, the system also updates its associated images. The following Active Storage updates occur:

UPDATE "action_text_rich_texts" SET "body" = .... WHERE "action_text_rich_texts"."id" = 1

UPDATE "active_storage_blobs" SET "metadata" = '{"identified":true}' WHERE "active_storage_blobs"."id" = 3

INSERT INTO "active_storage_attachments" ("name", "record_type", "record_id", "blob_id", "created_at") VALUES ('embeds', 'ActionText::RichText', 1, 3, '2025-03-31 11:46:13.464597')

Additionally, Rails updates the updated_at timestamp of the associated records:

UPDATE "products" SET "updated_at" = '2025-03-31 11:46:13.523640' WHERE "products"."id" = 1

Best Practices for Active Storage in Rails 8

  1. Use Direct Uploads: This improves performance by uploading files directly to cloud storage (e.g., AWS S3, Google Cloud Storage) instead of routing them through your Rails server.
  2. Attach Images Efficiently: Use has_one_attached or has_many_attached for file associations in models.
  3. Avoid Serving Files via Rails: Use a CDN or proxy service to serve images instead of relying on Rails controllers.
  4. Clean Up Unused Blobs: Regularly remove orphaned blob records using ActiveStorage::Blob.unattached.destroy_all.
  5. Optimize Image Processing: Use variants (image.variant(resize: "300x300").processed) to generate resized images efficiently.

In Rails 8, Active Storage uses two main tables for handling file uploads:

1. active_storage_blobs Table

This table stores metadata about the uploaded files but not the actual files. Each row represents a unique file (or “blob”) uploaded to Active Storage.

Columns in active_storage_blobs Table:

  • id – Unique identifier for the blob.
  • key – A unique key used to retrieve the file.
  • filename – The original name of the uploaded file.
  • content_type – The MIME type (e.g., image/jpeg, application/pdf).
  • metadata – JSON data storing additional information (e.g., width/height for images).
  • service_name – The storage service (e.g., local, amazon, google).
  • byte_size – File size in bytes.
  • checksum – A checksum to verify file integrity.
  • created_at – Timestamp when the file was uploaded.

Example Entry in active_storage_blobs:

INSERT INTO "active_storage_blobs" 
("key", "filename", "content_type", "service_name", "byte_size", "checksum", "created_at") 
VALUES ('avevnp6eg1xxxxxxsz8it6267eou7', 'floral-kurtha-2.jpg', 'image/jpeg', 'local', 204800, '0U0cXxxxxxxxxx/1u47Szg==', '2025-03-31 11:45:07.232453');

👉 Purpose: This table acts as a record of stored files and their metadata.


2. active_storage_attachments Table

This table links blobs (files) to Active Record models. Instead of storing files directly in the database, Rails stores a reference to the blob.

Columns in active_storage_attachments Table:

  • id – Unique identifier for the attachment.
  • name – Name of the attachment (:avatar, :images, etc.).
  • record_type – The model type associated with the file (User, Post, etc.).
  • record_id – The ID of the record in the model (users.id, posts.id).
  • blob_id – The corresponding ID from active_storage_blobs.
  • created_at – Timestamp when the association was created.

Example Entry in active_storage_attachments:

INSERT INTO "active_storage_attachments" 
("name", "record_type", "record_id", "blob_id", "created_at") 
VALUES ('avatar', 'User', 1, 42, '2025-03-31 08:15:20.123456');

INSERT INTO "active_storage_attachments" 
("name", "record_type", "record_id", "blob_id", "created_at") 
VALUES ('embeds', 'ActionText::RichText', 1, 4, '2025-03-31 11:46:20.123456');

👉 Purpose: This table allows a single file to be attached to multiple records without duplicating the file itself.


Why Does Rails Need Both Tables?

  1. Separation of Concerns:
    • active_storage_blobs tracks the files themselves.
    • active_storage_attachments links them to models.
  2. Efficient File Management:
    • The same file can be used in multiple places without storing it multiple times.
    • If a file is no longer attached to any record, Rails can remove it safely.
  3. Supports Different Attachments:
    • A model can have different types of attachments (avatar, cover_photo, documents).
    • A single model can have multiple files attached (has_many_attached).

Example Usage in Rails 8

class User < ApplicationRecord
  has_one_attached :avatar   # Single file
  has_many_attached :photos  # Multiple files
end

When a file is uploaded, an entry is added to active_storage_blobs, and an association is created in active_storage_attachments.

How Rails Queries These Tables

user.avatar # Fetches from `active_storage_blobs` via `active_storage_attachments`
user.photos.each { |photo| puts photo.filename } # Fetches multiple attached files

Conclusion

Rails 8 uses two tables to decouple file storage from model associations, enabling better efficiency, flexibility, and reusability. This structure allows models to reference files without duplicating them, making Active Storage a powerful solution for file management in Rails applications. 🚀


Where Are Files Stored in Rails 8 by Default?

By default, Rails 8 stores uploaded files using Active Storage’s disk service, meaning files are saved in the storage/ directory within your Rails project.

Default Storage Location:

  • Files are stored in:
    storage/
    ├── cache/ (temporary files)
    ├── store/ (permanent storage)
    └── variant/ (image transformations like resizing)
  • The exact file path inside storage/ is determined by the key column in the active_storage_blobs table. For example, if a blob entry has: key = 'xyz123abcd' then the file is stored at: storage/store/xyz123abcd

How to Change the Storage Location?

You can configure storage in config/storage.yml. For example:

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: us-east-1
  bucket: your_own_bucket-<%= Rails.env %>

Then, update config/environments/development.rb (or production.rb) to use:

config.active_storage.service = :local  # or :amazon for S3


How to Get the Stored File Path in Rails 8 Active Storage

Since Rails stores files in a structured directory inside storage/, the actual file path can be determined using the key stored in the active_storage_blobs table.

Get the File Path in Local Storage

If you’re using the Disk service (default for development and test), you can retrieve the stored file path manually:

blob = ActiveStorage::Blob.last
file_path = Rails.root.join("storage", "store", blob.key)
puts file_path

🔹 Example Output:

/your_project/storage/store/xyz123abcd

💡 This path is internal and cannot be accessed directly from a browser.


How to Get the File URL

Instead of accessing the internal path, Active Storage provides methods to generate URLs for public access.

1. Generate a URL for Direct Access

If you want a publicly accessible URL, you can use:

Rails.application.routes.url_helpers.rails_blob_url(blob, host: "http://localhost:3000")

🔹 Example Output:

http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiO...--filename.jpg

This redirects to the actual file storage location.

2. Get a Temporary Signed URL

For direct storage services like S3 or Google Cloud Storage, you can generate a signed URL:

blob.service_url

🔹 Example Output (for S3 storage):

https://your-s3-bucket.s3.amazonaws.com/xyz123abcd?X-Amz-Signature=...

🔹 Example Output (for local storage, using Disk service):

http://localhost:3000/rails/active_storage/disk/eyJfcmFpbHMiO...

This signed URL expires after a set time (default is a few minutes).

3. Get a Variant URL for an Image

If your file is an image and you want a resized version, use:

variant = blob.variant(resize: "300x300").processed
Rails.application.routes.url_helpers.rails_representation_url(variant, host: "http://localhost:3000")

🔹 Example Output:

http://localhost:3000/rails/active_storage/representations/abcdxyz.../resize_300x300.jpg

Summary

TaskCommand
Get internal file pathRails.root.join("storage", "store", blob.key)
Get public file URLRails.application.routes.url_helpers.rails_blob_url(blob, host: "http://localhost:3000")
Get signed (temporary) URL (If your model has has_one/many_attached)blob.service_url
Get resized image URLRails.application.routes.url_helpers.rails_representation_url(blob.variant(resize: "300x300").processed, host: "http://localhost:3000")
  • Files are stored in the storage/ directory by default.
  • Use rails_blob_url or service_url to get an accessible URL.
  • Use variant to generate resized versions.
  • For production, it’s best to use a cloud storage service like Amazon S3.

Understanding has_one_attached and has_many_attached in Rails 8

Rails 8 provides a built-in way to handle file attachments through Active Storage. The key methods for attaching files to models are:

  1. has_one_attached – For a single file attachment.
  2. has_many_attached – For multiple file attachments.

Let’s break down what they do and why they are useful.

1. has_one_attached

This is used when a model should have a single file attachment. For example, a User model may have only one profile picture.

Usage:

class User < ApplicationRecord
  has_one_attached :avatar
end

How It Works:

  • When you upload a file, Active Storage creates an entry in the active_storage_blobs table.
  • The active_storage_attachments table links this file to the record.
  • If a new file is attached, the old one is automatically replaced.

Example: Attaching and Displaying an Image

user = User.find(1)
user.avatar.attach(io: File.open("/path/to/avatar.jpg"), filename: "avatar.jpg", content_type: "image/jpeg")

# Checking if an avatar exists
user.avatar.attached? # => true

# Displaying the image in a view
<%= image_tag user.avatar.variant(resize: "100x100").processed if user.avatar.attached? %>

2. has_many_attached

Use this when a model can have multiple file attachments. For instance, a Product model may have multiple images.

Usage:

class Product < ApplicationRecord
  has_many_attached :images
end

How It Works:

  • Multiple files can be attached to a single record.
  • Active Storage tracks all file uploads in the active_storage_blobs and active_storage_attachments tables.
  • Deleting an attachment removes it from storage.

Example: Attaching and Displaying Multiple Images

product = Product.find(1)
product.images.attach([
  { io: File.open("/path/to/image1.jpg"), filename: "image1.jpg", content_type: "image/jpeg" },
  { io: File.open("/path/to/image2.jpg"), filename: "image2.jpg", content_type: "image/jpeg" }
])

# Checking if images exist
product.images.attached? # => true

# Displaying all images in a view
<% if product.images.attached? %>
  <% product.images.each do |image| %>
    <%= image_tag image.variant(resize: "200x200").processed %>
  <% end %>
<% end %>

Benefits of Using has_one_attached & has_many_attached

  1. Simplifies File Attachments – Directly associates files with Active Record models.
  2. No Need for Extra Tables – Unlike some gems (e.g., CarrierWave), Active Storage doesn’t require additional tables for storing file paths.
  3. Easy Cloud Storage Integration – Works seamlessly with Amazon S3, Google Cloud Storage, and Azure.
  4. Variant Processing – Generates resized versions of images using variant (e.g., thumbnails).
  5. Automatic Cleanup – Old attachments are automatically removed when replaced.

Final Thoughts

Active Storage in Rails 8 provides a seamless way to manage file uploads, integrating directly with models while handling storage efficiently. By understanding how it processes uploads internally, we can better optimize performance and ensure a smooth user experience.

In an upcoming blog, we’ll dive deeper into Turbo Streams and how they enhance real-time updates in Rails applications.

Setup 🛠 Rails 8 App – Part 2: Command line, VS Code, RuboCop, Server, Action text, Image processing

In the first part of this guide, we covered setting up a Rails 8 app with essential configurations. In this follow-up, we’ll go over optimizing command-line usage, setting up VS Code for development, running migrations, styling the app, and enabling Action Text.


1. Optimizing Command-Line Usage with Aliases

One of the best ways to speed up development is to create shortcuts for frequently used commands. You can do this by adding aliases to your shell configuration.

Steps to Add an Alias:

  1. Open your shell configuration file:
    vim ~/.zshrc
  2. Search for the alias section:
    <esc> / alias <enter>
  3. Add your alias:
    alias gs="git status"
  4. Save and exit:
    <esc> :wq
  5. Reload your configuration:
    source ~/.zshrc
  6. Use your new alias:
    gs

This method saves time by allowing you to run frequently used commands more quickly.


2. Using Terminal Efficiently in VS Code

By default, VS Code uses `Ctrl + “ to toggle the terminal, which may not be intuitive. You can change this shortcut:

  1. Open VS Code.
  2. Go to Settings → Keyboard Shortcuts.
  3. Search for Toggle Terminal.
  4. Click Edit and change it to Ctrl + Opt + T for easier access.

3. Setting Up RuboCop in VS Code

RuboCop ensures your Ruby code follows best practices. Here’s how to set it up:

Checking RuboCop from the Terminal:

rubocop .

VS Code Setup:

  1. Open Command Palette (Cmd + Shift + P) and search for “Lint by RuboCop”.
  2. Go to Extensions Tab and install “VS Code RuboCop”.
  3. In VS Code Settings, search for “Rubocop” and check Ruby -> Rubocop -> Execute Path.
  4. Find the RuboCop installation path: whereis rubocop Example output: ~/.local/share/mise/installs/ruby/3.4.1/bin/rubocop/
  5. Update the Execute Path in VS Code to: ~/.local/share/mise/installs/ruby/3.4.1/bin/
  6. If RuboCop still returns an empty output, check .rubocop.yml in your project: ~/rails/design_studio/.rubocop.yml
  7. If the issue persists, ensure the gem is installed: gem install rubocop
  8. Restart VS Code from the Rails project root: code .

For more details, check the official documentation: RuboCop Usage


4. Running Migrations and Starting the Server

Running Migrations:

rails db:migrate -t

You can check the file: db/schema.rb You can see the active storage tables for attachments and other info.

Starting the Rails Server:

bin/dev

Access your application at: http://localhost:3000/

Check the product routes:


5. Adding Basic Styles

To quickly improve the appearance of your site, add this to application.html.erb:

<%= stylesheet_link_tag "https://cdn.simplecss.org/simple.css" %>

Alternatively, use Tailwind CSS for a more modern approach.

An example of convention over configurations in view file in Rails:

<div id="products">
  <%= render brand.products %>
</div>

Rails will look into the views/products/ folder and fetch right partial file that match.


6. Debugging with Rails Console

If an error occurs, you can inspect variables and data in the Rails console:

rails console


7. Installing Action Text for Rich Text Editing

Action Text is not installed by default but can be added easily:

rails action_text:install

This command:

  • Installs JavaScript dependencies
  • Creates necessary stylesheets
  • Generates database migrations for rich text

Running Migrations:

rails db:migrate -t

If you encounter an error about missing gems:

Could not find gem 'image_processing (~> 1.2)'

Run:

bundle install
rails db:migrate -t

Updating the Product Model:

Add this to app/models/product.rb:

has_rich_text :description

Updating the Form View:

In app/views/products/_form.html.erb, replace the description input with:

<%= form.rich_text_area :description %>

Now, visiting the new product page should display a rich text editor.


8. Solving Image Processing Errors

If you see an error like:

LoadError: Could not open library 'vips.42'

Install libvips to resolve it:

brew install vips


Conclusion

In this second part, we covered:

  • Optimizing terminal usage with aliases.
  • Configuring VS Code for efficient development.
  • Running migrations and starting the Rails server.
  • Enhancing the UI with styles.
  • Debugging using Rails console.
  • Installing and configuring Action Text for rich text support.

Stay tuned for more Rails 8 improvements!

Part 3: https://railsdrop.com/2025/03/25/setup-rails-8-app-git-setup-gitignore-part-3/

To be continued… 🚀

Setup 🛠 Rails 8 App – Part 1: Setup All Necessary Configurations | Ruby | Rails setup | Kamal | Rails Generations

Ruby on Rails 8 introduces several improvements that make development easier, more secure, and more maintainable. In this guide, we’ll walk through setting up a new Rails 8 application while noting the significant configurations and features that come out of the box.

1. Check Your Ruby and Rails Versions

If not installed Ruby 3.4 and Rails 8.0 please check: https://railsdrop.com/2025/02/11/installing-and-setup-ruby-3-rails-8-vscode-ide-on-macos-in-2025/

Before starting, ensure that you have the correct versions of Ruby and Rails installed:

$ ruby -v
ruby 3.4.1

$ rails -v
Rails 8.0.1

If you don’t have these versions installed, update them using your package manager or version manager (like rbenv or RVM).

2. Create a New Rails 8 Application

Run the following command to create a new Rails app:

$ rails new design_studio

Noteworthy Files and Directories Created

Here are some interesting files and directories that are generated with a new Rails 8 app:

 create  .ruby-version
 create  bin/brakeman
 create  bin/rubocop
 create  bin/docker-entrypoint
 create  .rubocop.yml
 create  .github/workflows
 create  .github/workflows/ci.yml
 create  config/cable.yml
 create  config/storage.yml
 create  config/initializers/content_security_policy.rb
 create  config/initializers/filter_parameter_logging.rb
 create  config/initializers/new_framework_defaults_8_0.rb

Key Takeaways:

  • Security & Code Quality Tools: Brakeman (security scanner) and RuboCop (code style linter) are included by default.
  • Docker Support: The presence of bin/docker-entrypoint suggests better built-in support for containerized deployment.
  • GitHub Actions Workflow: The .github/workflows/ci.yml file provides default CI configurations.
  • Enhanced Security: The content_security_policy.rb initializer helps enforce a strict security policy.
  • New Rails Defaults: The new_framework_defaults_8_0.rb initializer helps manage breaking changes in Rails 8.

Rails automatically creates the following during the creation of the rails new app.

a. Configuring Import Maps and Installing Turbo & Stimulus

Rails 8 still defaults to Import Maps for JavaScript package management, avoiding the need for Node.js and Webpack:

$ rails turbo:install stimulus:install

This creates the following files:

create    config/importmap.rb
create    app/javascript/controllers
create    app/javascript/controllers/index.js
create    app/javascript/controllers/hello_controller.js
append    config/importmap.rb

Key Takeaways:

  • Import Maps: Defined in config/importmap.rb, allowing dependency management without npm.
  • Hotwired Support: Turbo and Stimulus are automatically configured for modern front-end development.
  • Generated Controllers: Stimulus controllers are pre-configured inside app/javascript/controllers/.

b. Deploying with Kamal

Kamal simplifies deployment with Docker and Kubernetes. Rails 8 includes built-in support:

$ bundle binstubs kamal
$ bundle exec kamal init

This results in:

Created .kamal/secrets file
Created sample hooks in .kamal/hooks

Key Takeaways:

  • Automated Deployment Setup: Kamal provides easy-to-use deployment scripts.
  • Secret Management: The .kamal/secrets file ensures secure handling of credentials.
  • Deployment Hooks: Custom hooks allow pre- and post-deployment scripts for automation.

c. Setting Up Caching and Queues with Solid Cache, Queue, and Cable

NOTE: Rails automatically creates this for you while creating the rails app.

Rails 8 includes Solid Cache, Solid Queue, and Solid Cable for enhanced performance and scalability:

$ rails solid_cache:install solid_queue:install solid_cable:install

This creates:

create  config/cache.yml
create  db/cache_schema.rb
create  config/queue.yml

Key Takeaways:

  • Caching Support: config/cache.yml manages application-wide caching.
  • Database-Powered Queue System: Solid Queue simplifies background job management without requiring external dependencies like Sidekiq.
  • Real-Time WebSockets: Solid Cable offers Action Cable improvements for real-time features.

3. Rails 8 Migration Enhancements

Rails 8 provides new shortcuts and syntax improvements for database migrations:

NOT NULL Constraints with ! Shortcut

You can impose NOT NULL constraints directly from the command line using !:

# Example for not null constraints: 
➜ rails generate model User name:string!

Type Modifiers in Migrations

Rails 8 allows passing commonly used type modifiers directly via the command line. These modifiers are enclosed in curly braces {} after the field type.

# Example for model generation: 
➜ rails generate model Product name:string description:text
# Example for passing modifiers: 
➜ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}

Generating a Scaffold for the Product Model

Let’s generate a complete scaffold for our Product model:

✗ rails generate scaffold product title:string! description:text category:string color:string 'size:string{10}' 'mrp:decimal{7,2}' 'discount:decimal{7,2}' 'rating:decimal{1,1}'
➜  design_studio git:(main) ✗ rails -v
Rails 8.0.1
➜  design_studio git:(main) ✗ ruby -v
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [arm64-darwin24]
➜  design_studio git:(main) ✗ rails generate scaffold product title:string! description:text category:string color:string 'size:string{10}' 'mrp:decimal{7,2}' 'discount:decimal{7,2}' 'rating:decimal{1,1}'

Using the Rails Resource Generator

The rails g resource command is a powerful way to generate models, controllers, migrations, and routes all in one go. This is particularly useful when setting up RESTful resources in a Rails application.

Basic Syntax

➜ rails g resource product

This command creates the necessary files for a new resource, including:

  • A model (app/models/product.rb)
  • A migration file (db/migrate/)
  • A controller (app/controllers/product_controller.rb)
  • Routes in config/routes.rb
  • A test file (test/controllers/product_controller_test.rb or spec/)

Example Usage

To generate a Post resource with attributes:

➜ rails g resource Product title:string! description:text brand:references

This will:

  1. Create a Product model with title and description attributes.
  2. Add a brand_id foreign key as a reference.
  3. Apply a NOT NULL constraint on title (! shortcut).
  4. Generate a corresponding migration file.
  5. Set up routes automatically (resources :products).

Running the Migration

After generating a resource, apply the migration to update the database:

➜ rails db:migrate


Difference Between resource and scaffold

Rails provides both rails g resource and rails g scaffold, but they serve different purposes:

Featurerails g resourcerails g scaffold
Generates a Model
Generates a Migration
Generates a Controller✅ (empty actions)✅ (full CRUD actions)
Generates Views (HTML/ERB)✅ (index, show, new, edit, etc.)
Generates Routes
Generates Helper Files
Generates Tests
  • rails g resource is minimal—it generates only essential files without view templates. It’s useful when you want more control over how your views and controller actions are built.
  • rails g scaffold is more opinionated and generates full CRUD functionality with prebuilt forms and views, making it ideal for rapid prototyping.

If you need full CRUD functionality quickly, use scaffold. If you prefer a leaner setup with manual control over implementation, use resource.

Conclusion

Rails 8 significantly enhances the development experience with built-in security tools, CI/CD workflows, streamlined deployment via Kamal, and modern front-end support with Turbo & Stimulus. It also improves caching, background jobs, and real-time features with Solid tools.

These improvements make Rails 8 a robust framework for modern web applications, reducing the need for additional dependencies while keeping development efficient and secure.

Enjoy Rails! 🚀

Writing Perfect Active Record 🗒️ Queries in Ruby on Rails 8

Active Record (AR) is the heart of Ruby on Rails when it comes to database interactions. Writing efficient and readable queries is crucial for application performance and maintainability. This guide will help you master Active Record queries with real-world examples and best practices.


Setting Up a Sample Database

To demonstrate complex Active Record queries, let’s create a Rails app with a sample database structure containing multiple tables.

Generate Models & Migrations

rails new MyApp --database=postgresql
cd MyApp
rails g model User name:string email:string
rails g model Post title:string body:text user:references
rails g model Comment body:text user:references post:references
rails g model Category name:string
rails g model PostCategory post:references category:references
rails g model Like user:references comment:references
rails db:migrate

Database Schema Overview

  • users: Stores user information.
  • posts: Stores blog posts written by users.
  • comments: Stores comments on posts, linked to users and posts.
  • categories: Stores post categories.
  • post_categories: Join table for posts and categories.
  • likes: Stores likes on comments by users.

Basic Active Record Queries

1. Fetching All Records

User.all  # Returns all users (Avoid using it directly on large datasets as it loads everything into memory)

⚠️ User.all can lead to performance issues if the table contains a large number of records. Instead, prefer pagination (User.limit(100).offset(0)) or batch processing (User.find_each).

2. Finding a Specific Record

User.find(1)  # Finds a user by ID
User.find_by(email: 'john@example.com')  # Finds by attribute

3. Filtering with where vs having

Post.where(user_id: 2)  # Fetch all posts by user with ID 2

Difference between where and having:

  • where is used for filtering records before grouping.
  • having is used for filtering after group operations.

Example:

Post.group(:user_id).having('COUNT(id) > ?', 5)  # Users with more than 5 posts

4. Ordering Results

User.order(:name)  # Order users alphabetically
Post.order(created_at: :desc)  # Order posts by newest first

5. Limiting Results

Post.limit(5)  # Get the first 5 posts

6. Selecting Specific Columns

User.select(:id, :name)  # Only fetch ID and name

7. Fetching Users with a Specific Email Domain

User.where("email LIKE ?", "%@gmail.com")

8. Fetching the Most Recent Posts

Post.order(created_at: :desc).limit(5)

9. Using pluck for Efficient Data Retrieval

User.pluck(:email)  # Fetch only emails as an array

10. Checking if a Record Exists Efficiently

User.exists?(email: 'john@example.com')

11. Including Associations (eager loading to avoid N+1 queries)

Post.includes(:comments).where(comments: { body: 'Great post!' })


Advanced Queries with Joins

1. Joining Tables (INNER JOIN)

Post.joins(:user).where(users: { name: 'John' })

2. Self Join Example

A self-join is useful when dealing with hierarchical relationships, such as an employee-manager structure.

Model Setup

class Employee < ApplicationRecord
  belongs_to :manager, class_name: 'Employee', optional: true
  has_many :subordinates, class_name: 'Employee', foreign_key: 'manager_id'
end

Sample Data

idnamemanager_id
1AliceNULL
2Bob1
3Carol1
4Dave2

Query: Find Employees Who Report to Alice

Employee.joins(:manager).where(managers_employees: { name: 'Alice' })

Result:

idnamemanager_id
2Bob1
3Carol1

This query fetches employees who report to Alice (i.e., those where manager_id = 1).

3. Fetching Users with No Posts (LEFT JOIN with NULL check)

User.left_outer_joins(:posts).where(posts: { id: nil })

4. Counting Posts Per User

User.joins(:posts).group('users.id').count

Complex Queries in Active Record

1. Fetching Posts with the Most Comments

Post.joins(:comments)
    .group('posts.id')
    .order('COUNT(comments.id) DESC')
    .limit(1)

2. Fetching Posts with More than 5 Comments

Post.joins(:comments)
    .group(:id)
    .having('COUNT(comments.id) > ?', 5)

3. Finding Users Who Liked the Most Comments

User.joins(comments: :likes)
    .group('users.id')
    .select('users.id, users.name, COUNT(likes.id) AS likes_count')
    .order('likes_count DESC')
    .limit(1)

4. Fetching Posts Belonging to Multiple Categories

Post.joins(:categories).group('posts.id').having('COUNT(categories.id) > ?', 1)

5. Fetching the Last Comment of Each Post

Comment.select('DISTINCT ON (post_id) *').order('post_id, created_at DESC')

6. Fetching Users Who Haven’t Commented on a Specific Post

User.where.not(id: Comment.where(post_id: 10).select(:user_id))

7. Fetching Users Who Have Commented on Every Post

User.joins(:comments).group(:id).having('COUNT(DISTINCT comments.post_id) = ?', Post.count)

8. Finding Posts With No Comments

Post.left_outer_joins(:comments).where(comments: { id: nil })

9. Fetching the User Who Created the Most Posts

User.joins(:posts)
    .group('users.id')
    .select('users.id, users.name, COUNT(posts.id) AS post_count')
    .order('post_count DESC')
    .limit(1)

10. Fetching the Most Liked Comment

Comment.joins(:likes)
    .group('comments.id')
    .order('COUNT(likes.id) DESC')
    .limit(1)

11. Fetching Comments with More than 3 Likes and Their Associated Posts

Comment.joins(:likes, :post)
    .group('comments.id', 'posts.id')
    .having('COUNT(likes.id) > ?', 3)

12. Finding Users Who Haven’t Liked Any Comments

User.left_outer_joins(:likes).where(likes: { id: nil })

13. Fetching Users, Their Posts, and the Count of Comments on Each Post

User.joins(posts: :comments)
    .group('users.id', 'posts.id')
    .select('users.id, users.name, posts.id AS post_id, COUNT(comments.id) AS comment_count')
    .order('comment_count DESC')

Importance of inverse_of in Model Associations

What is inverse_of?

The inverse_of option in Active Record associations helps Rails correctly link objects in memory, avoiding unnecessary database queries and ensuring bidirectional association consistency.

Example Usage

class User < ApplicationRecord
  has_many :posts, inverse_of: :user
end

class Post < ApplicationRecord
  belongs_to :user, inverse_of: :posts
end

Why Use inverse_of?

  • Performance Optimization: Prevents extra queries by using already loaded objects.
  • Ensures Data Consistency: Updates associations without additional database fetches.
  • Enables Nested Attributes: Helps when using accepts_nested_attributes_for.

Example:

user = User.new(name: 'Alice')
post = user.posts.build(title: 'First Post')
post.user == user  # True without needing an additional query

Best Practices to use in Rails Projects

1. Using Scopes for Readability

class Post < ApplicationRecord
  scope :recent, -> { order(created_at: :desc) }
end

Post.recent.limit(10)  # Fetch recent posts

2. Using find_each for Large Datasets

User.find_each(batch_size: 100) do |user|
  puts user.email
end

3. Avoiding SELECT * for Performance

User.select(:id, :name).load

4. Avoiding N+1 Queries with includes

Post.includes(:comments).each do |post|
  puts post.comments.count
end


Conclusion

Mastering Active Record queries is essential for writing performant and maintainable Rails applications. By using joins, scopes, batch processing, and eager loading, you can write clean and efficient queries that scale well.

Do you have any favorite Active Record query tricks? Share them in the comments!