Cookies are fundamental to web applications, but choosing the right storage method can make or break your app’s security and performance. Rails 7 offers multiple cookie storage mechanisms, each with distinct security properties and use cases. Let’s explore when to use each approach and why it matters.
Cookies are small text files created by a web server and stored on your device by your web browser when you visit a web application.
Because the basic HTTP protocol used on the web is stateless (meaning it treats every new page request as entirely brand new), cookies give web applications a memory. They allow the server to recognize your browser, remember who you are, and keep track of your state or actions as you move from page to page.
How Cookies Work
The Request: You visit a web application or log into a site.
The Creation: The web server sends an HTTP response containing a Set-Cookie header with specific data (like a unique ID or user preferences).
The Storage: Your browser saves this small text file locally on your computer or phone.
The Return: Whenever you click another link or load a new page on that same site, your browser automatically attaches the stored cookie back to the server in the request headers, letting the app know your status
The Cookie Storage Spectrum
Rails provides four main cookie storage methods, each offering different levels of security:
# 1. Plain cookies - readable and modifiable by client
cookies[:theme] = 'dark'
# 2. Signed cookies - readable but tamper-proof
cookies.signed[:discount_code] = 'SAVE10'
# 3. Encrypted cookies - hidden and tamper-proof
cookies.encrypted[:user_preferences] = { notifications: true }
# 4. Session storage - server-side with encrypted session cookie
session[:current_user_id] = user.id
1. Plain Cookies: When Transparency is Acceptable
Use for: Non-sensitive data where client-side reading/modification is acceptable or even desired.
Signed cookies prevent modification while remaining readable. Rails uses HMAC-SHA1 with your secret_key_base to create a cryptographic signature.
# Setting signed cookies
cookies.signed[:discount_code] = 'SAVE10'
cookies.signed[:referral_source] = 'google_ads'
# Reading signed cookies
discount = cookies.signed[:discount_code] # Returns 'SAVE10' or nil if tampered
# ❌ Don't store sensitive data in plain cookies
cookies[:ssn] = '123-45-6789' # Visible to everyone!
# ✅ Use appropriate security level
cookies.encrypted[:ssn] = '123-45-6789' # Hidden and protected
session[:user_id] = user.id # Server-side, encrypted
2. Set Proper Cookie Attributes
# Secure cookies for HTTPS
cookies[:theme] = {
value: 'dark',
secure: Rails.env.production?, # HTTPS only
httponly: true, # No JavaScript access
samesite: :strict # CSRF protection
}
3. Handle Cookie Tampering Gracefully
def current_discount_code
code_name = cookies.signed[:discount]
return nil unless code_name
DiscountCode.find_by(name: code_name)&.tap do |code|
# Remove if expired or invalid
cookies.delete(:discount) unless code.usable?
end
end
Create dedicated classes for complex cookie management:
class Session::CookieDiscountAccessor
def initialize(cookies)
@cookies = cookies
end
def discount_code
@cookies.signed[:discount] && DiscountCode.find_by(name: @cookies.signed[:discount])
end
def set_discount_code(code)
@cookies.signed[:discount] = {
value: code.name,
expires: code.expiration || 30.days.from_now
}
end
def remove_discount_code
@cookies.delete(:discount)
end
end
2. Validation and Cleanup
class Session::CheckAndRemoveDiscountCode
def initialize(cookies:)
@accessor = Session::CookieDiscountAccessor.new(cookies)
end
def run
# Remove referral conflicts
@accessor.referral_code && @accessor.remove_discount_code && return
# Remove expired codes
discount_code = @accessor.discount_code
@accessor.remove_discount_code if discount_code && !discount_code.usable?
end
end
3. Error Handling for Corrupted Cookies
def safe_read_encrypted_cookie(key)
cookies.encrypted[key]
rescue ActiveSupport::MessageVerifier::InvalidSignature,
ActiveSupport::MessageEncryptor::InvalidMessage
# Cookie was corrupted or created with different secret
cookies.delete(key)
nil
end
Performance Considerations
Cookie Size Limits
Total limit: 4KB per domain
Individual limit: ~4KB per cookie
Count limit: ~50 cookies per domain
CPU Overhead
# Benchmark different storage methods
require 'benchmark'
Benchmark.bm do |x|
x.report("plain") { 1000.times { cookies[:test] = 'value' } }
x.report("signed") { 1000.times { cookies.signed[:test] = 'value' } }
x.report("encrypted") { 1000.times { cookies.encrypted[:test] = 'value' } }
end
# Results (approximate):
# user system total real
# plain 0.001000 0.000000 0.001000 ( 0.001000)
# signed 0.010000 0.000000 0.010000 ( 0.009000)
# encrypted 0.050000 0.000000 0.050000 ( 0.048000)
# config/application.rb
config.force_ssl = true # HTTPS in production
# Use Secure Headers gem
SecureHeaders::Configuration.default do |config|
config.cookies = {
secure: true,
httponly: true,
samesite: {
lax: true
}
}
end
Testing Cookie Security
# spec/lib/session/coupon_code_spec.rb
RSpec.describe Session::CouponCode do
describe 'cookie tampering protection' do
it 'handles corrupted signed cookies gracefully' do
# Simulate tampered cookie
cookies.signed[:discount] = 'SAVE10'
cookies[:discount] = 'tampered_value' # Direct manipulation
accessor = Session::CookieDiscountAccessor.new(cookies)
expect(accessor.discount_code).to be_nil
end
end
end
Migration Strategies
Upgrading Cookie Security
def upgrade_cookie_security
# Read from old plain cookie
if (old_value = cookies[:legacy_data])
# Migrate to encrypted
cookies.encrypted[:legacy_data] = old_value
cookies.delete(:legacy_data)
end
end
Handling Secret Key Rotation
# config/credentials.yml.enc
secret_key_base: new_secret
legacy_secret_key_base: old_secret
# In application
def read_with_fallback(key)
cookies.encrypted[key] || begin
# Try with old secret
old_verifier = ActiveSupport::MessageEncryptor.new(
Rails.application.credentials.legacy_secret_key_base
)
old_verifier.decrypt_and_verify(cookies[key])
rescue
nil
end
end
Quick Decision Matrix
Data Type
Sensitivity
Client Access Needed
Recommended Storage
Theme preferences
Low
Yes
Plain cookies
Discount codes
Medium
No
Signed cookies
User settings
Medium
No
Encrypted cookies
Authentication
High
No
Session
Credit card data
High
No
Database + session ID
Shopping cart
Medium
No
Session or encrypted
CSRF tokens
High
Limited
Session (built-in)
Common Pitfalls to Avoid
Don’t mix storage types for the same data
# ❌ Inconsistent
cookies[:user_id] = user.id # Sometimes
cookies.signed[:user_id] = user.id # Other times
# ✅ Consistent
session[:user_id] = user.id # Always
Don’t store large objects in cookies
# ❌ Will hit 4KB limit
cookies.encrypted[:full_user] = user.to_json
# ✅ Store reference
session[:user_id] = user.id
Cookie storage in Rails 7 offers a rich toolkit for different security and performance needs. The key is matching the storage method to your data’s sensitivity and access patterns:
Plain cookies for non-sensitive, client-accessible data
Signed cookies when you need tamper protection but not confidentiality
Encrypted cookies for sensitive data that must remain client-side
Session storage for server-side state with automatic encryption
Remember: the best cookie strategy combines appropriate storage methods with proper security headers, validation, and graceful error handling. When in doubt, err on the side of more security rather than less.
The Rails cookie system is designed to make secure defaults easy—take advantage of it to build applications that are both performant and secure.
Ruby’s meta-programming capabilities let you write code that writes code, opening doors to DSLs (Check my post on DSL), dynamic behaviours, and DRY abstractions. In this two-part series, we’ll explore:
Part 1:class_eval, instance_eval, eval, define_method, and method_missing
Part 2: Additional metaprogramming methods like define_singleton_method, module_eval, send, and more
🏷️ Part 1: Core Metaprogramming Methods
🔨 1. What is class_eval?
class_eval is a method that allows you to evaluate a block of code or a string within the context of a class. It’s part of Ruby’s metaprogramming toolkit and is inherited from the Module class.
Basic Syntax
class MyClass
# class definition
end
MyClass.class_eval do
# code executed in the context of MyClass
def new_method
puts "Hello from class_eval!"
end
end
Key Features
1. Context Execution
The code inside class_eval is executed as if it were written directly inside the class definition:
class Example
def existing_method
puts "I exist"
end
end
Example.class_eval do
def dynamic_method
puts "I was added dynamically!"
end
end
# Now you can use the new method
obj = Example.new
obj.dynamic_method # => "I was added dynamically!"
2. String Evaluation
You can also pass a string instead of a block:
class MyClass
end
MyClass.class_eval("def hello; puts 'Hello!'; end")
3. Access to Class Variables and Constants
The evaluated code has access to the class’s variables and constants:
class Calculator
OPERATIONS = [:add, :subtract, :multiply, :divide]
end
Calculator.class_eval do
OPERATIONS.each do |op|
define_method(op) do |a, b|
case op
when :add then a + b
when :subtract then a - b
when :multiply then a * b
when :divide then a / b
end
end
end
end
Common Use Cases
1. Dynamic Method Creation
class User
attr_accessor :name, :email
end
# Dynamically add validation methods
User.class_eval do
[:name, :email].each do |field|
define_method("validate_#{field}") do
!send(field).nil? && !send(field).empty?
end
end
end
2. Plugin Systems
class Plugin
def self.register(plugin_name, &block)
class_eval(&block)
end
end
Plugin.register(:logger) do
def log(message)
puts "[LOG] #{message}"
end
end
3. Configuration DSLs
class Config
def self.configure(&block)
class_eval(&block)
end
end
Config.configure do
def database_url
"postgresql://localhost/myapp"
end
def api_key
ENV['API_KEY']
end
end
Differences from instance_eval
class_eval: Executes code in the context of the class (like being inside the class definition)
instance_eval: Executes code in the context of an instance of the class
class Example
def self.class_method
puts "I'm a class method"
end
def instance_method
puts "I'm an instance method"
end
end
# class_eval - can define class methods
Example.class_eval do
def self.new_class_method
puts "New class method"
end
end
# instance_eval - can define instance methods
Example.instance_eval do
def new_instance_method
puts "New instance method"
end
end
Security Considerations
⚠️ Warning: Using class_eval with user input can be dangerous:
# DANGEROUS - don't do this with user input
user_code = gets.chomp
MyClass.class_eval(user_code) # Could execute malicious code
Performance Notes
class_eval with blocks is generally faster than with strings
The block version is also safer and more readable
Use string evaluation only when you need to evaluate dynamic code
Real-World Example
Here’s how you might use class_eval in a practical scenario:
class ActiveRecord
def self.has_many(association_name)
class_eval do
define_method(association_name) do
# Implementation for has_many association
puts "Fetching #{association_name} for #{self.class}"
end
end
end
end
class User < ActiveRecord
has_many :posts
end
user = User.new
user.posts # => "Fetching posts for User"
class_eval is a powerful tool for meta-programming in Ruby, allowing you to dynamically modify classes at runtime. It’s commonly used in frameworks like Rails for creating DSLs and dynamic method generation.
🛠️ 2. What is instance_eval?
instance_eval is a method that allows you to evaluate a block of code or a string within the context of a specific object instance. It’s inherited from the BasicObject class and is available on all Ruby objects.
Basic Syntax
class MyClass
def initialize(name)
@name = name
end
end
obj = MyClass.new("Alice")
obj.instance_eval do
# code executed in the context of this specific object
puts @name # Can access instance variables
end
Key Features
1. Instance Context Execution
The code inside instance_eval is executed as if it were an instance method of the object:
class Person
def initialize(name, age)
@name = name
@age = age
end
def introduce
puts "Hi, I'm #{@name}"
end
end
person = Person.new("Bob", 30)
person.instance_eval do
puts "Name: #{@name}" # => "Name: Bob"
puts "Age: #{@age}" # => "Age: 30"
introduce # => "Hi, I'm Bob"
end
2. Access to Instance Variables
You can read and modify instance variables:
class BankAccount
def initialize(balance)
@balance = balance
end
end
account = BankAccount.new(1000)
account.instance_eval do
puts "Current balance: #{@balance}" # => "Current balance: 1000"
@balance += 500 # Modify instance variable
puts "New balance: #{@balance}" # => "New balance: 1500"
end
3. String Evaluation
You can also pass a string instead of a block:
class Example
def initialize(value)
@value = value
end
end
obj = Example.new("test")
obj.instance_eval("puts @value") # => "test"
Common Use Cases
1. Testing Private Methods
class Calculator
private
def secret_calculation(x, y)
x * y + 42
end
end
calc = Calculator.new
# Access private method in tests
result = calc.instance_eval { secret_calculation(5, 3) }
puts result # => 57
2. Dynamic Property Access
class Config
def initialize
@settings = {}
end
def set(key, value)
@settings[key] = value
end
end
config = Config.new
config.set(:api_url, "https://api.example.com")
config.set(:timeout, 30)
# Access settings dynamically
config.instance_eval do
puts @settings[:api_url] # => "https://api.example.com"
puts @settings[:timeout] # => 30
end
3. Object Inspection and Debugging
class ComplexObject
def initialize
@data = { a: 1, b: 2, c: 3 }
@metadata = { created: Time.now }
end
end
obj = ComplexObject.new
obj.instance_eval do
puts "All instance variables:"
instance_variables.each do |var|
puts "#{var}: #{instance_variable_get(var)}"
end
end
4. DSL (Domain Specific Language) Implementation
class Builder
def initialize
@result = []
end
def build(&block)
instance_eval(&block)
@result
end
end
builder = Builder.new
result = builder.build do
@result << "item1"
@result << "item2"
@result << "item3"
end
puts result # => ["item1", "item2", "item3"]
Differences from class_eval
Feature
instance_eval
class_eval
Context
Object instance
Class
Self
Points to the object
Points to the class
Method Definition
Defines instance methods
Defines instance methods
Access
Instance variables, private methods
Class methods, constants
class Example
def initialize
@value = "instance value"
end
@@class_var = "class variable"
end
obj = Example.new
# instance_eval - context is the object
obj.instance_eval do
puts @value # => "instance value"
puts self.class # => Example
end
# class_eval - context is the class
Example.class_eval do
puts @@class_var # => "class variable"
puts self # => Example
end
Advanced Examples
1. Method Chaining with instance_eval
class QueryBuilder
def initialize
@conditions = []
end
def where(field, value)
@conditions << "#{field} = '#{value}'"
self
end
def build
@conditions.join(" AND ")
end
end
query = QueryBuilder.new.instance_eval do
where("name", "John")
where("age", 25)
build
end
puts query # => "name = 'John' AND age = '25'"
2. Configuration Objects
class AppConfig
def initialize
@config = {}
end
def configure(&block)
instance_eval(&block)
end
def database(url)
@config[:database] = url
end
def api_key(key)
@config[:api_key] = key
end
def get_config
@config
end
end
config = AppConfig.new
config.configure do
database "postgresql://localhost/myapp"
api_key ENV['API_KEY']
end
puts config.get_config
3. Object Serialization
class Serializable
def to_hash
result = {}
instance_eval do
instance_variables.each do |var|
key = var.to_s.delete('@').to_sym
result[key] = instance_variable_get(var)
end
end
result
end
end
class User < Serializable
def initialize(name, email)
@name = name
@email = email
end
end
user = User.new("Alice", "alice@example.com")
puts user.to_hash # => {:name=>"Alice", :email=>"alice@example.com"}
Security Considerations
⚠️ Warning: Like class_eval, using instance_eval with user input can be dangerous:
# DANGEROUS - don't do this with user input
user_code = gets.chomp
obj.instance_eval(user_code) # Could execute malicious code
Performance Notes
instance_eval with blocks is faster than with strings
The block version is safer and more readable
Use string evaluation only when necessary for dynamic code
Real-World Usage
instance_eval is commonly used in:
Testing frameworks (RSpec, Minitest)
Configuration systems (Rails initializers)
Builder patterns (ActiveRecord queries)
DSL implementations (Rake tasks, Capistrano)
# RSpec example
describe User do
it "has a name" do
user = User.new("John")
expect(user.instance_eval { @name }).to eq("John")
end
end
instance_eval is a powerful tool for metaprogramming that allows you to execute code in the context of any object, making it invaluable for testing, debugging, and creating flexible APIs.
⚡ 3. What is eval?
Ruby’s eval method, which is the most powerful and potentially dangerous meta-programming tool in Ruby.
eval is a method that evaluates a string as Ruby code in the current context. It’s inherited from the Kernel module and is available everywhere in Ruby. Unlike class_eval and instance_eval, eval executes code in the current binding (the current execution context).
Basic Syntax
# Basic eval usage
eval("puts 'Hello from eval!'")
# With variables
x = 10
y = 20
result = eval("x + y")
puts result # => 30
Key Features
1. Current Context Execution
eval executes code in the current binding (local variables, methods, etc.):
name = "Alice"
age = 25
city = "New York"
eval("puts \"#{name} is #{age} years old and lives in #{city}\"")
# => "Alice is 25 years old and lives in New York"
3. Dynamic Method Calls
class Calculator
def add(x, y)
x + y
end
def multiply(x, y)
x * y
end
end
calc = Calculator.new
method_name = "add"
args = [5, 3]
result = eval("calc.#{method_name}(#{args.join(', ')})")
puts result # => 8
class MathEvaluator
def self.evaluate(expression)
eval(expression)
rescue => e
"Error: #{e.message}"
end
end
puts MathEvaluator.evaluate("2 + 3 * 4") # => 14
puts MathEvaluator.evaluate("Math.sqrt(16)") # => 4.0
puts MathEvaluator.evaluate("10 / 0") # => "Error: divided by 0"
3. Template Processing
class Template
def initialize(template)
@template = template
end
def render(binding)
eval("\"#{@template}\"", binding)
end
end
template = Template.new("Hello <%= name %>, you are <%= age %> years old!")
name = "Bob"
age = 30
result = template.render(binding)
puts result # => "Hello Bob, you are 30 years old!"
4. Code Generation
class CodeGenerator
def self.generate_method(method_name, body)
eval("
def #{method_name}
#{body}
end
")
end
end
class MyClass
CodeGenerator.generate_method(:greet, "puts 'Hello, World!'")
CodeGenerator.generate_method(:calculate, "2 + 2")
end
obj = MyClass.new
obj.greet # => "Hello, World!"
puts obj.calculate # => 4
Advanced Examples
1. Dynamic Class Creation
class DynamicClassCreator
def self.create_class(class_name, methods = {})
eval("
class #{class_name}
#{methods.map { |name, body| "def #{name}; #{body}; end" }.join("\n")}
end
")
end
end
DynamicClassCreator.create_class("Person", {
greet: "puts 'Hello!'",
age: "25"
})
person = Person.new
person.greet # => "Hello!"
puts person.age # => 25
2. Expression Parser
class ExpressionParser
def self.parse(expression, variables = {})
# Create a safe binding with variables
binding_obj = binding
variables.each do |key, value|
binding_obj.local_variable_set(key, value)
end
eval(expression, binding_obj)
end
end
result = ExpressionParser.parse("x * y + z", { x: 5, y: 3, z: 10 })
puts result # => 25
⚠️ CRITICAL WARNING: eval is extremely dangerous when used with untrusted input!
Dangerous Examples:
# NEVER do this with user input!
user_input = gets.chomp
eval(user_input) # User could input: system('rm -rf /')
# Dangerous with file input
file_content = File.read('user_provided_file.rb')
eval(file_content) # Could contain malicious code
Safe Alternatives:
# Instead of eval, use safer alternatives
class SafeCalculator
def self.calculate(expression)
# Use a math library or parser instead
case expression
when /^\d+\s*\+\s*\d+$/
# Safe addition
eval(expression)
else
raise "Unsafe expression"
end
end
end
Binding and Context
1. Custom Binding
def create_binding(variables = {})
binding_obj = binding
variables.each do |key, value|
binding_obj.local_variable_set(key, value)
end
binding_obj
end
my_binding = create_binding(x: 10, y: 20)
result = eval("x + y", my_binding)
puts result # => 30
2. Different Contexts
class ContextExample
def initialize(value)
@value = value
end
def evaluate_in_context(code)
eval(code)
end
end
obj = ContextExample.new("test")
result = obj.evaluate_in_context("@value")
puts result # => "test"
Performance Considerations
eval is slower than direct code execution
The string must be parsed every time
No compile-time optimization
Use sparingly and only when necessary
Best Practices
1. Avoid When Possible
# Bad - using eval
method_name = "calculate"
eval("result = #{method_name}(5, 3)")
# Good - using send
result = send(method_name, 5, 3)
2. Whitelist Allowed Operations
class SafeEvaluator
ALLOWED_OPERATIONS = %w[+ - * /]
def self.evaluate(expression)
operation = expression.match(/(\d+)\s*([+\-*/])\s*(\d+)/)
return "Invalid expression" unless operation
op = operation[2]
return "Operation not allowed" unless ALLOWED_OPERATIONS.include?(op)
eval(expression)
end
end
3. Use Sandboxing
class SandboxedEval
def self.safe_eval(code, timeout: 1)
Timeout::timeout(timeout) do
eval(code)
end
rescue Timeout::Error
"Execution timed out"
rescue => e
"Error: #{e.message}"
end
end
Real-World Usage
eval is used in:
Template engines (ERB, Haml)
Configuration systems (Rails initializers)
Code generators (Rails generators)
REPLs (Interactive Ruby shells)
# ERB template example
require 'erb'
template = ERB.new("Hello <%= name %>!")
name = "World"
result = template.result(binding)
puts result # => "Hello World!"
Summary
eval is the most powerful metaprogramming tool in Ruby, but also the most dangerous. Use it only when:
You have complete control over the input
No safer alternative exists
You understand the security implications
You implement proper validation and sandboxing
For most use cases, prefer safer alternatives like send, method_missing, or dedicated parsing libraries.
✍️ 4. What is define_method?
Ruby’s define_method, which is a powerful meta-programming tool for dynamically creating methods at runtime.
define_method is a method that allows you to dynamically define instance methods on a class or module. It’s inherited from the Module class and is a key tool for meta-programming in Ruby.
Basic Syntax
class MyClass
define_method :dynamic_method do |*args|
puts "Hello from dynamic method with args: #{args}"
end
end
obj = MyClass.new
obj.dynamic_method("test", 123) # => "Hello from dynamic method with args: [\"test\", 123]"
Key Features
1. Dynamic Method Creation
class Calculator
# Define methods dynamically based on operations
[:add, :subtract, :multiply, :divide].each do |operation|
define_method operation do |a, b|
case operation
when :add then a + b
when :subtract then a - b
when :multiply then a * b
when :divide then a / b
end
end
end
end
calc = Calculator.new
puts calc.add(5, 3) # => 8
puts calc.multiply(4, 2) # => 8
puts calc.divide(10, 2) # => 5
2. Access to Instance Variables
class Person
def initialize(name, age)
@name = name
@age = age
end
# Create getter methods dynamically
[:name, :age].each do |attribute|
define_method attribute do
instance_variable_get("@#{attribute}")
end
end
end
person = Person.new("Alice", 25)
puts person.name # => "Alice"
puts person.age # => 25
3. Method with Parameters
class DynamicAPI
define_method :api_call do |endpoint, params = {}|
puts "Calling #{endpoint} with params: #{params}"
# Simulate API call
"Response from #{endpoint}"
end
end
api = DynamicAPI.new
api.api_call("/users", { id: 1 }) # => "Calling /users with params: {:id=>1}"
Common Use Cases
1. Attribute Accessors
class Model
def self.attr_accessor(*attributes)
attributes.each do |attribute|
# Define getter
define_method attribute do
instance_variable_get("@#{attribute}")
end
# Define setter
define_method "#{attribute}=" do |value|
instance_variable_set("@#{attribute}", value)
end
end
end
attr_accessor :name, :email, :age
end
user = Model.new
user.name = "John"
user.email = "john@example.com"
puts user.name # => "John"
puts user.email # => "john@example.com"
2. Validation Methods
class User
def initialize(attributes = {})
attributes.each do |key, value|
instance_variable_set("@#{key}", value)
end
end
# Create validation methods dynamically
[:name, :email, :age].each do |field|
define_method "validate_#{field}" do
value = instance_variable_get("@#{field}")
case field
when :name
!value.nil? && !value.empty?
when :email
value =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
when :age
value.is_a?(Integer) && value > 0 && value < 150
end
end
end
end
user = User.new(name: "Alice", email: "alice@example.com", age: 25)
puts user.validate_name # => true
puts user.validate_email # => true
puts user.validate_age # => true
3. API Method Generation
class APIClient
def initialize(base_url)
@base_url = base_url
end
# Generate API methods for different resources
[:users, :posts, :comments].each do |resource|
define_method "get_#{resource}" do |id = nil|
endpoint = id ? "#{resource}/#{id}" : resource
puts "GET #{@base_url}/#{endpoint}"
# Actual HTTP request would go here
end
define_method "create_#{resource.singularize}" do |data|
puts "POST #{@base_url}/#{resource}"
puts "Data: #{data}"
# Actual HTTP request would go here
end
end
end
api = APIClient.new("https://api.example.com")
api.get_users # => "GET https://api.example.com/users"
api.get_users(123) # => "GET https://api.example.com/users/123"
api.create_user(name: "John") # => "POST https://api.example.com/users"
4. Database Query Methods
class ActiveRecord
def self.has_many(association_name)
define_method association_name do
puts "Fetching #{association_name} for #{self.class}"
# Actual database query would go here
[]
end
end
def self.belongs_to(association_name)
define_method association_name do
puts "Fetching #{association_name} for #{self.class}"
# Actual database query would go here
nil
end
end
end
class User < ActiveRecord
has_many :posts
belongs_to :company
end
user = User.new
user.posts # => "Fetching posts for User"
user.company # => "Fetching company for User"
Advanced Examples
1. Method with Dynamic Logic
class DynamicCalculator
def self.create_operation(operation_name, &block)
define_method operation_name do |*args|
instance_eval(&block)
end
end
create_operation :custom_add do
args = method(__method__).parameters.map { |_, name| local_variable_get(name) }
args.inject(:+)
end
create_operation :power do
base, exponent = method(__method__).parameters.map { |_, name| local_variable_get(name) }
base ** exponent
end
end
calc = DynamicCalculator.new
puts calc.custom_add(1, 2, 3, 4) # => 10
puts calc.power(2, 3) # => 8
2. Conditional Method Definition
class FeatureToggle
def self.define_feature_method(feature_name, enabled = true)
if enabled
define_method feature_name do
puts "Feature #{feature_name} is enabled"
# Feature implementation
end
else
define_method feature_name do
puts "Feature #{feature_name} is disabled"
# Fallback or no-op
end
end
end
define_feature_method :new_ui, true
define_feature_method :beta_feature, false
end
app = FeatureToggle.new
app.new_ui # => "Feature new_ui is enabled"
app.beta_feature # => "Feature beta_feature is disabled"
3. Method with Different Signatures
class FlexibleAPI
def self.define_flexible_method(method_name)
define_method method_name do |*args, **kwargs, &block|
puts "Method: #{method_name}"
puts "Arguments: #{args}"
puts "Keyword arguments: #{kwargs}"
puts "Block given: #{block_given?}"
# Process based on arguments
if block_given?
block.call(args.first)
elsif !kwargs.empty?
kwargs.values.first
else
args.first
end
end
end
define_flexible_method :process
end
api = FlexibleAPI.new
api.process("hello") # => "hello"
api.process(data: "world") # => "world"
api.process("test") { |x| x.upcase } # => "TEST"
4. Method Aliasing
class MethodAliaser
def self.create_aliases(base_method, *aliases)
aliases.each do |alias_name|
define_method alias_name do |*args, &block|
send(base_method, *args, &block)
end
end
end
def original_method
puts "Original method called"
end
create_aliases :original_method, :alias1, :alias2, :alternative_name
end
obj = MethodAliaser.new
obj.alias1 # => "Original method called"
obj.alternative_name # => "Original method called"
Performance Considerations
1. Method Definition Timing
class PerformanceExample
# Methods defined at class definition time (faster)
define_method :static_method do
puts "Static method"
end
def self.create_dynamic_methods
# Methods defined at runtime (slower)
1000.times do |i|
define_method "dynamic_method_#{i}" do
puts "Dynamic method #{i}"
end
end
end
end
2. Memory Usage
class MemoryEfficient
# More memory efficient - methods defined once
METHODS = [:method1, :method2, :method3]
METHODS.each do |method_name|
define_method method_name do
puts "Called #{method_name}"
end
end
end
Best Practices
1. Use Meaningful Names
# Good
define_method :calculate_total do
# implementation
end
# Bad
define_method :m1 do
# implementation
end
2. Handle Errors Gracefully
class SafeMethodDefiner
def self.define_safe_method(method_name, &block)
define_method method_name do |*args|
begin
instance_eval(&block)
rescue => e
puts "Error in #{method_name}: #{e.message}"
nil
end
end
end
end
3. Document Dynamic Methods
class DocumentedClass
# Dynamically creates getter methods for attributes
# @param attributes [Array<Symbol>] list of attribute names
def self.create_getters(*attributes)
attributes.each do |attr|
define_method attr do
instance_variable_get("@#{attr}")
end
end
end
create_getters :name, :email
end
Real-World Usage
define_method is commonly used in:
Rails ActiveRecord (associations, validations)
RSpec (dynamic test methods)
Configuration systems (dynamic setters/getters)
API clients (dynamic endpoint methods)
ORM frameworks (dynamic query methods)
# Rails-like example
class ActiveRecord
def self.validates_presence_of(*attributes)
attributes.each do |attribute|
define_method "validate_#{attribute}_presence" do
value = send(attribute)
value.nil? || value.to_s.empty?
end
end
end
end
class User < ActiveRecord
validates_presence_of :name, :email
end
define_method is a powerful tool for creating flexible, dynamic APIs and reducing code duplication through meta-programming.
🌀 5. What is method_missing?
Ruby’s method_missing, which is a powerful meta-programming tool that allows you to handle calls to undefined methods dynamically.
method_missing is a special method in Ruby that gets called automatically when an object receives a message (method call) for a method that doesn’t exist. It’s inherited from the BasicObject class and is a key component of Ruby’s dynamic nature.
Basic Syntax
class MyClass
def method_missing(method_name, *args, &block)
puts "Method '#{method_name}' called with args: #{args}"
# Handle the missing method call
end
end
obj = MyClass.new
obj.undefined_method("hello", 123) # => "Method 'undefined_method' called with args: [\"hello\", 123]"
Key Features
1. Automatic Invocation
class DynamicHandler
def method_missing(method_name, *args, &block)
puts "Trying to call: #{method_name}"
puts "Arguments: #{args}"
puts "Block given: #{block_given?}"
# Return a default value or handle the call
"Handled by method_missing"
end
end
obj = DynamicHandler.new
result = obj.some_random_method("arg1", "arg2") { puts "block" }
puts result
# Output:
# Trying to call: some_random_method
# Arguments: ["arg1", "arg2"]
# Block given: true
# Handled by method_missing
2. Method Name and Arguments
class FlexibleAPI
def method_missing(method_name, *args, **kwargs, &block)
puts "Method: #{method_name}"
puts "Arguments: #{args}"
puts "Keyword arguments: #{kwargs}"
# Handle different method patterns
case method_name.to_s
when /^get_(.+)$/
"Getting #{$1}"
when /^set_(.+)$/
"Setting #{$1} to #{args.first}"
else
"Unknown method: #{method_name}"
end
end
end
api = FlexibleAPI.new
puts api.get_user_info # => "Getting user_info"
puts api.set_name("Alice") # => "Setting name to Alice"
puts api.random_method # => "Unknown method: random_method"
Common Use Cases
1. Dynamic Property Access
class DynamicProperties
def initialize
@data = {}
end
def method_missing(method_name, *args)
method_str = method_name.to_s
if method_str.end_with?('=')
# Setter method
property_name = method_str.chomp('=')
@data[property_name] = args.first
else
# Getter method
@data[method_str]
end
end
end
obj = DynamicProperties.new
obj.name = "Alice"
obj.age = 25
obj.city = "New York"
puts obj.name # => "Alice"
puts obj.age # => 25
puts obj.city # => "New York"
2. API Method Generation
class APIClient
def initialize(base_url)
@base_url = base_url
end
def method_missing(method_name, *args)
method_str = method_name.to_s
case method_str
when /^get_(.+)$/
resource = $1
puts "GET #{@base_url}/#{resource}"
# Actual HTTP GET request
when /^post_(.+)$/
resource = $1
data = args.first || {}
puts "POST #{@base_url}/#{resource}"
puts "Data: #{data}"
# Actual HTTP POST request
when /^put_(.+)$/
resource = $1
data = args.first || {}
puts "PUT #{@base_url}/#{resource}"
puts "Data: #{data}"
# Actual HTTP PUT request
when /^delete_(.+)$/
resource = $1
puts "DELETE #{@base_url}/#{resource}"
# Actual HTTP DELETE request
else
super
end
end
end
api = APIClient.new("https://api.example.com")
api.get_users # => "GET https://api.example.com/users"
api.post_user(name: "John") # => "POST https://api.example.com/user"
api.put_user(1, name: "Jane") # => "PUT https://api.example.com/user"
api.delete_user(1) # => "DELETE https://api.example.com/user"
3. Configuration DSL
class Configuration
def initialize
@config = {}
end
def method_missing(method_name, *args)
method_str = method_name.to_s
if method_str.end_with?('=')
# Setter
key = method_str.chomp('=')
@config[key] = args.first
else
# Getter
@config[method_str]
end
end
def to_hash
@config
end
end
config = Configuration.new
config.database_url = "postgresql://localhost/myapp"
config.api_key = ENV['API_KEY']
config.debug_mode = true
puts config.database_url # => "postgresql://localhost/myapp"
puts config.to_hash # => {"database_url"=>"postgresql://localhost/myapp", ...}
4. Builder Pattern
class HTMLBuilder
def initialize
@html = []
end
def method_missing(tag_name, *args, &block)
content = args.first || ""
attributes = args.last.is_a?(Hash) ? args.last : {}
if block_given?
@html << "<#{tag_name}#{format_attributes(attributes)}>"
@html << yield
@html << "</#{tag_name}>"
else
@html << "<#{tag_name}#{format_attributes(attributes)}>#{content}</#{tag_name}>"
end
self
end
private
def format_attributes(attributes)
return "" if attributes.empty?
" " + attributes.map { |k, v| "#{k}=\"#{v}\"" }.join(" ")
end
def to_s
@html.join("\n")
end
end
builder = HTMLBuilder.new
html = builder.html do
builder.head do
builder.title("My Page")
end
builder.body(class: "main") do
builder.h1("Hello World")
builder.p("This is a paragraph", class: "intro")
end
end
puts html
# Output:
# <html>
# <head>
# <title>My Page</title>
# </head>
# <body class="main">
# <h1>Hello World</h1>
# <p class="intro">This is a paragraph</p>
# </body>
# </html>
Advanced Examples
1. Method Caching with respond_to_missing?
class CachedMethodHandler
def initialize
@cache = {}
end
def method_missing(method_name, *args)
method_str = method_name.to_s
# Check if we should handle this method
if method_str.start_with?('cached_')
cache_key = "#{method_str}_#{args.hash}"
if @cache.key?(cache_key)
puts "Returning cached result for #{method_name}"
@cache[cache_key]
else
puts "Computing result for #{method_name}"
result = compute_result(method_str, args)
@cache[cache_key] = result
result
end
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?('cached_') || super
end
private
def compute_result(method_name, args)
# Simulate expensive computation
sleep(1)
"Result for #{method_name} with #{args}"
end
end
handler = CachedMethodHandler.new
puts handler.cached_expensive_calculation(1, 2, 3) # Slow
puts handler.cached_expensive_calculation(1, 2, 3) # Fast (cached)
puts handler.respond_to?(:cached_expensive_calculation) # => true
2. Dynamic Delegation
class Delegator
def initialize(target)
@target = target
end
def method_missing(method_name, *args, &block)
if @target.respond_to?(method_name)
@target.send(method_name, *args, &block)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
@target.respond_to?(method_name, include_private) || super
end
end
class User
def initialize(name, email)
@name = name
@email = email
end
def display_info
"Name: #{@name}, Email: #{@email}"
end
end
user = User.new("Alice", "alice@example.com")
delegator = Delegator.new(user)
puts delegator.display_info # => "Name: Alice, Email: alice@example.com"
3. Method Chaining with method_missing
class QueryBuilder
def initialize
@conditions = []
@order_by = nil
@limit = nil
end
def method_missing(method_name, *args)
method_str = method_name.to_s
case method_str
when /^where_(.+)$/
field = $1
value = args.first
@conditions << "#{field} = '#{value}'"
self
when /^order_by_(.+)$/
field = $1
direction = args.first || 'ASC'
@order_by = "#{field} #{direction}"
self
when /^limit$/
@limit = args.first
self
when /^execute$/
build_query
else
super
end
end
private
def build_query
query = "SELECT * FROM table"
query += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
query += " ORDER BY #{@order_by}" if @order_by
query += " LIMIT #{@limit}" if @limit
query
end
end
query = QueryBuilder.new
result = query.where_name("John")
.where_age(25)
.order_by_created_at("DESC")
.limit(10)
.execute
puts result
# => "SELECT * FROM table WHERE name = 'John' AND age = '25' ORDER BY created_at DESC LIMIT 10"
4. Event Handling
class EventHandler
def initialize
@handlers = {}
end
def method_missing(event_name, *args)
method_str = event_name.to_s
if method_str.end_with?('=')
# Register event handler
handler_name = method_str.chomp('=')
@handlers[handler_name] = args.first
else
# Trigger event
handler = @handlers[method_str]
if handler
handler.call(*args)
else
puts "No handler registered for event: #{method_str}"
end
end
end
end
handler = EventHandler.new
# Register event handlers
handler.on_click = ->(x, y) { puts "Clicked at (#{x}, #{y})" }
handler.on_hover = ->(element) { puts "Hovered over #{element}" }
# Trigger events
handler.on_click(100, 200) # => "Clicked at (100, 200)"
handler.on_hover("button") # => "Hovered over button"
handler.on_keypress # => "No handler registered for event: on_keypress"
Important Considerations
1. respond_to_missing?
Always implement respond_to_missing? when using method_missing:
class ProperHandler
def method_missing(method_name, *args)
if method_name.to_s.start_with?('dynamic_')
"Handled: #{method_name}"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?('dynamic_') || super
end
end
obj = ProperHandler.new
puts obj.respond_to?(:dynamic_method) # => true
puts obj.respond_to?(:real_method) # => false
2. Performance Impact
class PerformanceExample
def method_missing(method_name, *args)
# This gets called for EVERY undefined method
# Can be slow if called frequently
puts "Handling #{method_name}"
end
end
# Better approach: Define methods when first called
class BetterExample
def method_missing(method_name, *args)
if method_name.to_s.start_with?('dynamic_')
# Define the method for future calls
self.class.define_method(method_name) do |*method_args|
"Handled: #{method_name} with #{method_args}"
end
# Call the newly defined method
send(method_name, *args)
else
super
end
end
end
3. Debugging
class DebuggableHandler
def method_missing(method_name, *args, &block)
puts "DEBUG: method_missing called for #{method_name}"
puts "DEBUG: arguments: #{args}"
puts "DEBUG: block given: #{block_given?}"
# Your handling logic here
super
end
end
Real-World Usage
method_missing is commonly used in:
Rails ActiveRecord (dynamic finders)
RSpec (dynamic matchers)
Sinatra (dynamic route handlers)
Configuration systems (dynamic setters/getters)
API clients (dynamic endpoint methods)
# Rails-like example
class ActiveRecord
def method_missing(method_name, *args)
method_str = method_name.to_s
if method_str.start_with?('find_by_')
field = method_str.sub('find_by_', '')
value = args.first
puts "Finding record where #{field} = #{value}"
# Actual database query
else
super
end
end
end
class User < ActiveRecord
end
User.find_by_name("Alice") # => "Finding record where name = Alice"
User.find_by_email("alice@example.com") # => "Finding record where email = alice@example.com"
method_missing is a powerful tool for creating flexible, dynamic APIs, but should be used carefully with proper implementation of respond_to_missing? and consideration for performance.
🏷️ Part 2: Other Metaprogramming Tools
Ruby provides many more hooks for dynamic behavior:
🔗 1. define_singleton_method
Creates a method on a single object, similar to instance_eval + def.
str = "hello"
def str.shout; upcase + '!'; end
# vs.
str.define_singleton_method(:whisper) { downcase + '...' }
📦 2. module_eval / module_exec
Like class_eval but in a module’s context, useful to mix in behavior.
module Mixin; end
Mixin.module_eval do
def helper; "I'm mixed in"; end
end
🧩 3. send / public_send
Invoke methods by name, bypassing visibility with send, or respecting it with public_send.
Both send and public_send in Ruby allow you to dynamically call methods on an object by name (as a symbol or string). But the difference lies in method visibility (public, private, protected).
Key Difference
Method
Can call private/protected methods?
Example Usage
send
✅ Yes
"hello".send(:upcase)
public_send
❌ No (only public methods)
"hello".public_send(:upcase)
Example
class MyClass
def public_method
"I'm public"
end
private
def secret_method
"I'm private"
end
end
obj = MyClass.new
puts obj.send(:public_method) # ✅ Works: "I'm public"
puts obj.public_send(:public_method) # ✅ Works: "I'm public"
puts obj.send(:secret_method) # ✅ Works (can access private method)
puts obj.public_send(:secret_method) # ❌ Raises NoMethodError
? Why this matters
send is very powerful (and dangerous) because it ignores method visibility. It’s often used in metaprogramming (e.g., ActiveRecord uses it internally).
public_send is safer because it respects encapsulation. Use this when you don’t want to accidentally call private methods.
Best Practice
Use public_send whenever possible (especially if the method name comes from user input) to avoid security issues.
Use send only when you explicitly need access to private methods (e.g., in testing or metaprogramming).
Open the singleton class to define methods or mixins.
class << obj
def special; 'only me'; end
end
📡 8. autoload / require_relative
Delay loading of modules until first reference.
autoload :Parser, 'parser'
📐 9. respond_to_missing?
Complement method_missing to accurately report capabilities.
def respond_to_missing?(m, include_private=false)
@target.respond_to?(m) || super
end
🔀 10. Refinements
Scoped monkey-patching without global impact.
module StringExtensions
refine String do
def shout; upcase; end
end
end
using StringExtensions
puts 'hi'.shout # => HI
✅ Conclusion
Ruby’s metaprogramming toolbox is vast—from the core five methods in Part 1 to the advanced techniques in Part 2. By mastering these, you can write highly dynamic, DRY, and expressive code, but always balance power with clarity and maintainability.
Happy Ruby coding! 🚀
🔍 1. Why Use class_eval and instance_eval?
Ruby classes and objects are open, meaning you can modify them at runtime.
class_eval lets you inject instance methods into a class after it’s defined.
instance_eval lets you add singleton methods or access private state on a single object.
✨ Dynamic Method Generation
When you don’t know ahead of time what methods you’ll need—say you’re reading an external schema or configuration—you can’t hand‑write every method. Metaprogramming with class_eval/instance_eval generates them on the fly.
🔧 Building DSLs
Internal DSLs (e.g. Rails’ routing or configuration blocks) rely on evaluating blocks in the right context:
# routes.rb
Rails.application.routes.draw do
resources :users do
resources :posts
end
end
Here, instance_eval on the routing object makes resources available inside the block.
If you know all your methods at design time, define them normally. Lean on metaprogramming when you need flexibility, DRY generation, or want to craft a clean DSL.
🛠️ 2. When to Use define_method?
define_method is Ruby’s block‑based way to dynamically add methods in a class or module—safer than eval and can close over local variables:
class Serializer
%i[name age email].each do |attr|
define_method("serialize_#{attr}") do |user|
user.public_send(attr).to_s
end
end
end
Ideal for generating many similar methods (attribute serializers, dynamic finders).
Keeps code DRY without string interpolation.
📦 3. Ruby’s *arg and **arg Operators
⭐ Single Splat: *args
Definition: Packs extra positional args into an array, or unpacks an array into individual arguments. def foo(a, *others) p a # first arg p others # rest as an array end foo(1, 2, 3) # a=1, others=[2,3] foo(*[4,5,6]) # same as foo(4,5,6)
✨ Double Splat: **kwargs
Definition: Packs extra keyword args into a hash, or unpacks a hash into keyword arguments. def bar(x:, **opts) p x # required keyword p opts # other keywords in a hash end bar(x: 10, y: 20, z: 30) # opts={y:20, z:30} bar(**{x:1, y:2}) # same as bar(x:1, y:2)
📚 4. What Is a DSL? Main Ruby DSLs in Rails
A DSL (Domain‑Specific Language) is an internal API that reads like its own language, tailored to a task. Rails ships with many:
🚦 Routing DSL
Rails.application.routes.draw do
resources :users
end
Below is a practical, production-ready approach that covers controller hooks, controllers, models/libs, background jobs, and more—illustrated with a real scenario from Session::CouponCode.
Core principles
Keep transport (HTTP, JSON) in controllers; keep domain logic in models/libs.
Map known, expected failures to specific HTTP statuses.
Log unexpected failures; return a generic message to clients.
Centralize API error rendering in a base controller.
1) A single error boundary for all API controllers
Create a base Error::ApiError and rescue it (plus a safe catch‑all) in your ApiController.
# lib/error/api_error.rb
module Error
class ApiError < StandardError
attr_reader :status, :details
def initialize(message, status = :unprocessable_entity, details: nil)
super(message)
@status = status
@details = details
end
end
end
Order matters. Specific rescue_from before StandardError.
This pattern avoids duplicating rescue_from across controllers and keeps HTML controllers unaffected.
2) Errors in before actions
Because before_action runs inside controllers, the same rescue_from handlers apply.
Two patterns:
Render in the hook for simple guard clauses:
before_action :require_current_client
def require_current_client
return if current_client
render json: { success: false, error: 'require_login' }, status: :unauthorized
end
Raise a domain/auth error and let rescue_from handle JSON:
# lib/error/unauthorized_error.rb
module Error
class UnauthorizedError < Error::ApiError
def initialize(message = 'require_login') = super(message, :unauthorized)
end
end
before_action :require_current_client
def require_current_client
raise Error::UnauthorizedError unless current_client
end
Prefer raising if you want consistent global handling and logging.
3) Errors inside controllers
Use explicit renders for happy-path control flow; raise for domain failures:
def create
form = CreateThingForm.new(params.require(:thing).permit(:name))
result = CreateThing.new(form: form).call
if result.success?
render json: { success: true, thing: result.thing }, status: :created
else
# Known domain failure → raise an ApiError to map to 422
raise Error::ApiError.new(result.message, :unprocessable_entity, details: result.details)
end
end
Common controller exceptions (auto-mapped above):
ActionController::ParameterMissing → 400
ActiveRecord::RecordNotFound → 404
ActiveRecord::RecordInvalid → 422
ActiveRecord::RecordNotUnique → 409
4) Errors in models, services, and libs
Do not call render here. Either:
Return a result object (Success/Failure), or
Raise a domain‑specific exception that the controller maps to an HTTP response.
Example from our scenario, Session::CouponCode:
# lib/error/session/coupon_code_error.rb
module Error
module Session
class CouponCodeError < Error::ApiError; end
end
end
# lib/session/coupon_code.rb
class Session::CouponCode
def discount_dollars
# ...
case
when coupon_code.gift_card?
# ...
when coupon_code.discount_code?
# ...
when coupon_code.multiorder_discount_code?
# ...
else
raise Error::Session::CouponCodeError, 'Unrecognized discount code'
end
end
end
Then, in ApiController, the specific handler (or the Error::ApiError handler) renders JSON with a 422.
This preserves separation: models/libs raise; controllers decide HTTP.
5) Other important surfaces
ActiveJob / Sidekiq
Prefer retry_on, discard_on, and job‑level rescue with logging.
Return no HTTP here; jobs are async.
class MyJob < ApplicationJob
retry_on Net::OpenTimeout, wait: 10.seconds, attempts: 3
discard_on Error::ApiError
rescue_from(StandardError) { |e| Rollbar.error(e) }
end
Mailers
Use rescue_from to avoid bubble‑ups crashing deliveries:
class ApplicationMailer < ActionMailer::Base
rescue_from Postmark::InactiveRecipientError, Postmark::InvalidEmailRequestError do
# no-op / log
end
end
Routing / 404
For APIs, keep 404 mapping at the controller boundary with rescue_from ActiveRecord::RecordNotFound.
For HTML, config.exceptions_app = routes + ErrorsController.
Middleware / Rack
For truly global concerns, use middleware. This is rarely necessary for controller-scoped API errors in Rails.
Validation vs. Exceptions
Use validations (ActiveModel/ActiveRecord) for expected user errors.
Raise exceptions for exceptional conditions (invariants violated, external systems fail unexpectedly).
6) Observability
Always log unexpected errors in the catch‑all (StandardError).
Ruby, the language that brought joy back into programming, is now over two decades old. It revolutionized web development through Rails and championed a developer-first philosophy. But in the era of AI, server-less, and systems programming, is Ruby still relevant? With Python dominating AI, Go owning the backend space, and Elixir praised for concurrency — where does Ruby stand?
Let’s explore Ruby’s current state, the challenges it faces, and what the future might hold.
🧱 What Ruby Still Does Exceptionally Well
1. Web Development with Rails
Ruby on Rails remains one of the fastest and most pleasant ways to build web applications. It’s productive, expressive, and mature.
Companies like GitHub, Shopify, Basecamp, and Hey.com still use Rails at scale.
Rails 8 introduced modern features like Turbo, Hotwire, and Kamal (for zero-downtime deploys).
It’s still a top pick for startups wanting to build MVPs quickly.
2. Developer Happiness
The principle of “developer happiness” is deeply embedded in Ruby’s philosophy:
Intuitive syntax
Expressive and readable code
A community that values elegance over boilerplate
Ruby continues to be one of the best languages for teaching programming, prototyping ideas, or building software that feels joyful to write.
⚠️ Challenges Facing Ruby Today
1. Performance Limitations
Ruby’s performance has improved dramatically with YJIT, MJIT, and better memory handling. But it still lags behind languages like Go or Rust in raw speed, especially in CPU-bound or concurrent environments.
2. Concurrency and Parallelism
Ruby has a Global Interpreter Lock (GIL) in MRI, which limits real parallelism.
While Fibers and async gems (async, polyphony, concurrent-ruby) help, it’s not as seamless as Go’s goroutines or Elixir’s lightweight processes.
3. Ecosystem Narrowness
Ruby’s ecosystem is tightly tied to Rails.
Unlike Python, which powers AI, data science, and automation…
Or JavaScript, which rules the browser and serverless space…
Ruby hasn’t made significant inroads outside web development.
4. Enterprise Perception
Many large enterprises shy away from Ruby, viewing it as either:
A “legacy startup language“, or
Too dynamic and flexible for highly-regulated or enterprise-scale environments.
🛠️ How Can Ruby Improve?
💡 1. Concurrency and Async Programming
Embrace the shift toward non-blocking IO, async/await patterns.
Invest in the ecosystem around async, falcon, and evented web servers.
💡 2. AI/ML Integration
Ruby doesn’t need to compete with Python in AI, but it can bridge to Python using gems like pycall, pybind11, or ruby-dlib.
Better interop with other platforms like JRuby, TruffleRuby, or even WebAssembly can unlock new domains.
💡 3. Broaden Ecosystem Use
Encourage usage outside web: CLI tools, static site generation, scripting, DevOps, etc.
Frameworks like Hanami, Roda, Dry-rb, and Trailblazer are promising.
💡 4. Stronger Developer Outreach
More documentation, YouTube tutorials, free courses, and evangelism.
Encourage open source contribution in tools beyond Rails.
📉 Will Rails Usage Decline?
Not disappear, but become more specialized.
Rails is no longer the hottest framework — but it’s still one of the most productive and complete options for web development.
Startups love it for speed of development.
Mid-sized businesses rely on it for stability and maintainability.
But serverless-first, JavaScript-heavy, or cloud-native stacks may bypass it in favor of Next.js, Go, or Elixir/Phoenix.
The challenge is staying competitive in the face of frameworks that promise better real-time capabilities and lightweight microservices.
🌟 Why Ruby Still Matters
Despite all that, Ruby still offers:
🧘♂️ Developer productivity
🧩 Readable, expressive syntax
🚀 Fast prototyping
❤️ A helpful, mature community
🧪 First-class TDD culture
It’s a joy to write in Ruby. For many developers, that alone is enough.
🔚 Final Thoughts: The Joyful Underdog
Ruby is no longer the main character in the programming language race. But that’s okay.
In a world chasing performance benchmarks, Ruby quietly reminds us: “Programming can still be beautiful.“
The future of Ruby lies in:
Focusing on what it does best (developer experience, productivity)
Expanding into new areas (concurrency, scripting, interop)
And adapting — not by competing with Go or Python, but by embracing its unique strengths.
Welcome to my new series where I combine the power of Ruby with the discipline of Test-Driven Development (TDD) to tackle popular algorithm problems from LeetCode! 🧑💻💎 Whether you’re a Ruby enthusiast looking to sharpen your problem-solving skills, or a developer curious about how TDD can transform the way you approach coding challenges, you’re in the right place.
🎲 Episode 7: Minimum Size Subarray Sum
###########################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose @sum is greater than or equal to target. If there is no such subarray, return 0 instead.
#
# Example 1:
#
# Input: target = 7, nums = [2,3,1,2,4,3]
# Output: 2
# Explanation: The subarray [4,3] has the minimal length under the problem constraint.
# Example 2:
#
# Input: target = 4, nums = [1,4,4]
# Output: 1
# Example 3:
#
# Input: target = 11, nums = [1,1,1,1,1,1,1,1]
# Output: 0
#
#
# Constraints:
#
# 1 <= target <= 109
# 1 <= nums.length <= 105
# 1 <= nums[i] <= 104
#
###########################################################
# ❌ Fail
# frozen_string_literal: true
#######################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose sum is greater than or equal to target. If there is no such subarray, return 0 instead.
#
#######################################################
require 'minitest/autorun'
require_relative 'subarray_sum_min_size'
class TestSubArraySumMinSize < Minitest::Test
def set_up; end
def test_array_of_length_one
assert_equal 0, SubArray.new([2], 3).min_size
assert_equal 1, SubArray.new([2], 2).min_size
assert_equal 0, SubArray.new([3], 4).min_size
end
end
Source Code:
# frozen_string_literal: true
# disable rubocop GuardClause for better readability in the code
###########################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose @sum is greater than or equal to target. If there is no such subarray, return 0 instead.
# ............
#
###########################################################
class SubArray
def min_size
end
end
✗ ruby test_subarray_sum_min_size.rb
Run options: --seed 5914
# Running:
E
Finished in 0.000386s, 2590.6736 runs/s, 0.0000 assertions/s.
1) Error:
TestSubArraySumMinSize#test_array_of_length_one:
ArgumentError: wrong number of arguments (given 2, expected 0)
test_subarray_sum_min_size.rb:16:in 'BasicObject#initialize'
test_subarray_sum_min_size.rb:16:in 'Class#new'
test_subarray_sum_min_size.rb:16:in 'TestSubArraySumMinSize#test_array_of_length_one'
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
➜ minimum-size-subarray-sum git:(main) ✗
✅ Green: Making it pass
# Pass ✅
# frozen_string_literal: true
###########################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose sum is greater than or equal to target. If there is no such subarray, return 0 instead.
#
# Example 1:
#........
#
###########################################################
class SubArray
def initialize(nums, target)
@nums = nums
@target = target
end
def min_size
0 if @nums.length == 1 && @nums.first < @target
end
end
# Solution for upto 5 Array Input Length ✅
# frozen_string_literal: true
# disable rubocop GuardClause for better readability in the code
# rubocop:disable Style/GuardClause
###########################################################
# ...............
###########################################################
class SubArray
def initialize(nums, target)
@nums = nums
@target = target
@min_length = 0 # default 0 -> solution not found
@left_pos = 0
@right_pos = 0
@sum = nil
end
def min_size
while @right_pos < @nums.length
# first position where left and right positions are at starting point
@sum = if @left_pos.zero? && @right_pos.zero?
@nums[@right_pos]
else
# add elements inside the window
@nums[@left_pos..@right_pos].sum
end
if solution_found?
update_min_length
return 1 if @min_length == 1 # best scenario found, stop here
else
@right_pos += 1 # increase window size by 1
end
end
@min_length
end
private
def update_min_length
new_length = @right_pos - @left_pos + 1
if min_length_empty? || min_or_equal_length?(new_length)
@min_length = new_length
@left_pos += 1
end
end
def solution_found?
@sum >= @target
end
def min_length_empty?
@min_length.zero?
end
# if new length of subarray found is less than already found min length
# or new length found is equal to previous min length (should decrease window size
# by increasing left pos to find the less length subarray)
def min_or_equal_length?(new_length)
new_length <= @min_length
end
end
# Solution 1 ✅
# frozen_string_literal: true
# disable rubocop GuardClause for better readability in the code
# rubocop:disable Style/GuardClause
###########################################################
# #209
# .............
###########################################################
class SubArray
def initialize(nums, target)
@nums = nums
@target = target
@min_length = 0 # default 0 -> solution not found
@left_pos = 0
@right_pos = 0
@sum = nil
end
def min_size
while @right_pos < @nums.length
@sum = calculate_sum
if solution_found?
update_min_length
return 1 if @min_length == 1 # best scenario found, stop here
else
@right_pos += 1 # increase window size by 1
end
end
@min_length
end
private
def calculate_sum
# first position where left and right positions are at starting point
return @nums[@right_pos] if @left_pos.zero? && @right_pos.zero?
# add elements inside the window
@nums[@left_pos..@right_pos].sum
end
def update_min_length
new_length = @right_pos - @left_pos + 1
if min_length_empty? || min_or_equal_length?(new_length)
@min_length = new_length
@left_pos += 1
end
end
def solution_found?
@sum >= @target
end
def min_length_empty?
@min_length.zero?
end
# if new length of subarray found is less than already found min length
# or new length found is equal to previous min length (should decrease window size
# by increasing left pos to find the less length subarray)
def min_or_equal_length?(new_length)
new_length <= @min_length
end
end
# Solution 2 ✅
# frozen_string_literal: true
# disable rubocop GuardClause for better readability in the code
###########################################################
# #209
# .............
###########################################################
class SubArray
def initialize(nums, target)
@nums = nums
@target = target
@min_length = 0 # default 0 -> solution not found
@left_pos = 0
@right_pos = 0
@sum = nil
end
def min_size
while @right_pos < @nums.length
@sum = calculate_sum
if solution_found?
update_min_length
return 1 if @min_length == 1 # best scenario found, stop here
else
@right_pos += 1 # increase window size by 1
end
end
@min_length
end
private
def calculate_sum
# first position where left and right positions are at starting point
return @nums[@right_pos] if @left_pos.zero? && @right_pos.zero?
# add elements inside the window
@nums[@left_pos..@right_pos].sum
end
def update_min_length
new_length = @right_pos - @left_pos + 1
@min_length = new_length if min_length_empty? || min_length_greater?(new_length)
@left_pos += 1
end
def solution_found?
@sum >= @target
end
def min_length_empty?
@min_length.zero?
end
# if new length of subarray found is less than already found min length
# or new length found is equal to previous min length (should decrease window size
# by increasing left pos to find the less length subarray)
def min_length_greater?(new_length)
@min_length > new_length
end
end
🧮 Algorithm Complexity Analysis
Time Complexity: O(n²)
Our current algorithm has quadratic time complexity due to the calculate_sum method:
def calculate_sum(nums, left_pos, right_pos)
# This line causes O(n) complexity in each iteration
nums[left_pos..right_pos].sum
end
Solution: We should change this logic of repeated addition of numbers that are already added before. We can add the next Number (Right position) and substract the Left Number that is out of the window.
Space Complexity: O(1)
Only uses a constant number of variables regardless of input size
No additional data structures that grow with input
🚀 Optimized Version (O(n) Time):
Here’s how to make it linear time complexity:
Let’s Try to Optimize our solution with the Solution given above:
# frozen_string_literal: true
# disable rubocop GuardClause for better readability in the code
###########################################################
# ..................
###########################################################
class SubArray
def initialize(nums, target)
@nums = nums
@target = target
@min_length = 0 # default 0 -> solution not found
@left_pos = 0
@right_pos = 0
@sum = 0
end
def min_size
while @right_pos < @nums.length
# Add the new element at right_pos to the current sum
@sum += @nums[@right_pos]
update_min_length if solution_found?
@right_pos += 1 # always move right pointer
end
@min_length
end
private
def update_min_length
new_length = @right_pos - @left_pos + 1
@min_length = new_length if min_length_empty? || min_length_greater?(new_length)
# Shrink the window from the left as much as possible while maintaining sum >= target
while @left_pos < @right_pos && (@sum - @nums[@left_pos]) >= @target
@sum -= @nums[@left_pos]
@left_pos += 1
new_length = @right_pos - @left_pos + 1
@min_length = new_length if min_length_greater?(new_length)
end
end
def solution_found?
@sum >= @target
end
def min_length_empty?
@min_length.zero?
end
# if new length of subarray found is less than already found min length
# or new length found is equal to previous min length (should decrease window size
# by increasing left pos to find the less length subarray)
def min_length_greater?(new_length)
@min_length > new_length
end
end
📊 Complexity Comparison:
Version
Time Complexity
Space Complexity
Why
Your Current
O(n²)
O(1)
Recalculates sum each time
Optimized
O(n)
O(1)
Maintains running sum
Key Optimization:
Instead of recalculating the sum each time:
# Your approach (O(n) each time)
nums[left_pos..right_pos].sum
# Optimized approach (O(1) each time)
current_sum += num # Add new element
current_sum -= nums[left] # Remove old element
Our algorithm works correctly but can be optimized from O(n²) to O(n) time complexity!
LeetCode Submission (simplified version of 0(n)):
# @param {Integer} target
# @param {Integer[]} nums
# @return {Integer}
def min_sub_array_len(target, nums)
return 0 if nums.empty?
min_length = Float::INFINITY
left = 0
sum = 0
nums.each_with_index do |num, right|
sum += num
# Shrink window from left as much as possible while maintaining sum >= target
while sum >= target && left <= right
min_length = [min_length, right - left + 1].min
sum -= nums[left]
left += 1
end
end
min_length == Float::INFINITY ? 0 : min_length
end
Software development is not just about writing code; it’s about building high-quality, maintainable, and scalable systems that deliver value to users. To achieve this consistently, teams follow structured approaches known as software development methodologies. These methodologies provide a roadmap for planning, designing, developing, testing, and delivering software.
In this three-part blog series, we’ll explore key methodologies and best practices in software development, using Ruby and Ruby on Rails examples wherever appropriate.
🌐 What Are Software Development Methodologies?
Software development methodologies are structured processes or frameworks that guide the planning and execution of software projects. They help teams manage complexity, collaborate effectively, reduce risk, and deliver projects on time.
Common Goals of Any Methodology:
Define clear project scope and goals
Break down work into manageable tasks
Encourage communication among team members
Track progress and measure success
Deliver working software iteratively or incrementally
💼 Why Methodologies Matter
Without a methodology, software projects often suffer from unclear requirements, missed deadlines, buggy releases, or scope creep. A good methodology:
Increases team productivity
Ensures better quality and maintainability
Reduces time-to-market
Improves customer satisfaction
In Ruby and Rails projects, where rapid development is a key feature, following a methodology keeps things under control and makes collaboration more effective.
📖 A Brief Overview of Popular Software Development Methodologies
We’ll explore these in detail in Part 2, but here are the major ones:
1. Waterfall
A linear approach where each phase (Requirements → Design → Implementation → Testing → Deployment) happens one after another.
2. Agile
An iterative and incremental model that encourages adaptive planning, early delivery, and continuous improvement.
3. Scrum
A popular Agile framework that structures work in sprints and emphasizes team roles, ceremonies, and artifacts.
4. Kanban
A flow-based Agile method that uses visual boards to manage work and improve process efficiency.
5. Extreme Programming (XP)
Focuses on engineering excellence, including practices like pair programming, TDD, and continuous integration. Ruby and Rails communities highly embrace XP.
6. DevOps
Combines software development and IT operations to shorten the development lifecycle and deliver high-quality software continuously.
✨ Ruby On Rails: A Natural Fit for Agile and XP
Ruby and Rails were built with developer happiness and productivity in mind. They naturally complement Agile and XP methodologies.
Example:
Ruby on Rails encourages convention over configuration, which speeds up development.
It supports Test-Driven Development (TDD) out of the box via tools like RSpec and Minitest.
Features like scaffolding align well with the iterative nature of Agile.
🔹 Coming Up in Part 2
In the next part of this series, I’ll do a deep dive into each of the above methodologies, with pros, cons, and real-world use cases. I’ll also show examples of how Rails developers apply these in practice.
If you want to avoid the ActiveAdmin complexity entirely:
RailsAdmin:
gem 'rails_admin'
✅ No asset pipeline issues
✅ Works with Propshaft out of the box
❌ Less customizable than ActiveAdmin
Trestle:
gem 'trestle'
✅ Modern Bootstrap-based admin
✅ Better Rails 8 compatibility
❌ Smaller community
Adminterface (Deprecated):
🚫 No longer maintained (as shown in search results)
🏆 My Recommendation:
For our production app:Stick with Sprockets + ActiveAdmin 3.x
Why?
Stability – Battle-tested combination
Zero configuration issues
Full feature set available
Easy maintenance
The performance difference between Propshaft and Sprockets won’t significantly impact your admin interface, which is typically used by a small number of internal users.
🔮 Future Outlook:
ActiveAdmin v4 will eventually have better Propshaft support
Rails community is working on solutions
Node.js dependency trend for complex CSS is becoming standard anyway
Bottom Line: Don’t let the asset pipeline take over the application preferences. Our current Sprockets setup is perfectly fine for an admin interface! 🎯
The Reality Check:
We completely replaced Propshaft with Sprockets for the entire application.
Current State:
❌ Propshaft: Completely removed
✅ Sprockets: Handles ALL assets (main app + admin)
✅ Importmaps: Still working (unaffected)
✅ Hotwire/Turbo: Still working (unaffected)
🏗️ How ActiveAdmin Works:
ActiveAdmin is NOT a Rails Engine. It integrates directly into your app:
Routes: ActiveAdmin.routes(self) adds /admin routes to your app
Assets: ActiveAdmin’s CSS/JS becomes part of your asset pipeline
Controllers: ActiveAdmin generates controllers in your app’s namespace
It’s tightly coupled to your application, not isolated.
🤷♂️ So What Are Our Real Options?
Option 1: Accept Current Setup
What we have:
Modern Rails 8 with Importmaps + Hotwire ✅
Sprockets instead of Propshaft ❌ (but works fine)
Full-featured admin interface ✅
Option 2: Use activeadmin_assets Gem
# Replace current ActiveAdmin setup
gem 'activeadmin_assets' # Pre-compiled assets
gem 'propshaft' # Back to Rails 8 default
✅ Get Propshaft back
✅ Keep Rails 8 defaults
❌ Can’t customize admin styles
Option 3: Build Custom Admin
Keep your Rails 8 purity but build admin from scratch:
✅ Pure Rails 8
❌ Weeks of development time
🔍 What activeadmin_assets Actually Does:
The activeadmin_assets gem doesn’t replace ActiveAdmin – it provides pre-compiled static assets for ActiveAdmin to work with Propshaft.
But here’s the critical issue for you:
🚨 activeadmin_assets = NO Customization
If you use activeadmin_assets, you get:
✅ ActiveAdmin functionality
✅ Propshaft compatibility
❌ ZERO ability to customize styles/interface
The assets are pre-compiled and frozen – you can’t modify colors, layouts, or styling.
Since We Want Customization:
We’re actually in the BEST setup possible right now!
Our Current Setup Gives You:
✅ Full ActiveAdmin customization (styles, layouts, themes)
✅ Modern Rails 8 (Importmaps, Hotwire, Turbo)
✅ All ActiveAdmin features (filters, actions, forms)
❌ Sprockets instead of Propshaft (minor trade-off)
Customization Examples We Can Do:
Custom CSS themes and colors
Layout modifications
Custom JavaScript behaviors
Brand styling and logos
Advanced form customizations
🎯 Reality Check:
There’s no solution that gives us:
Full ActiveAdmin customization AND
Propshaft AND
Zero complexity
We have to pick 2 out of 3.
💡 My Honest Last Decision:
Keep our current setup! We’re already using:
Modern Rails 8 patterns (95% of your codebase)
Importmaps and Hotwire (exactly as intended)
Sprockets only affects asset serving (invisible to users)
The Propshaft vs Sprockets difference won’t affect your product’s success, but losing admin customization will limit our UX.
Let’s move on to quick development of more react components now. Before that let’s check what we have now and understand it very clear.
📄 File 1:
Our app/javascript/components/App.jsx file:
import React from 'react';
function App() {
return (
<div>
<h1>React is working fine!</h1>
<p>Welcome to Rails + React App</p>
</div>
);
}
export default App;
Let’s examine this React component step by step:
Line 1: Import React
import React from 'react';
import – ES6 module syntax to bring in external code
React – The main React library
from 'react' – Importing from the npm package named “react”
Why needed? Even though we use --jsx=automatic, we still import React for any hooks or React features we might use.
Function Component: Line 3-9
A React function component is a simple JavaScript function that serves as a building block for user interfaces in React applications. These components are designed to be reusable and self-contained, encapsulating a specific part of the UI and its associated logic.
function App() {
return (
<div>
<h1>React is working fine!</h1>
<p>Welcome to Rails + React App</p>
</div>
);
}
🔍 Breaking this down:
Line 3: Component Declaration
function App() {
function App() – This is a React Function Component
Component naming – Must start with capital letter (App, not app)
What it is – A JavaScript function that returns JSX (user interface)
Line 4-8: JSX Return
return (
<div>
<h1>React is working fine!</h1>
<p>Welcome to Rails + React App</p>
</div>
);
return – Every React component must return something
JSX – Looks like HTML, but it’s actually JavaScript
<div> – Must have one parent element (React Fragment rule)
<h1> & <p> – Regular HTML elements, but processed by React
Line 11: Export
export default App;
export default – ES6 syntax to make this component available to other files
App – The component name we’re exporting
Why needed? So application.js can import and use this component
📄 File 2:
Our app/javascript/application.js file:
// Entry point for the build script in your package.json
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './components/App';
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('react-root');
if(container) {
const root = createRoot(container);
root.render(<App />);
}
});
This is the entry point that connects React to your Rails app:
Imports: Line 2-4
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './components/App';
🔍 Breaking down each import:
Line 2:
import React from 'react';
Same as before – importing the React library
Line 3:
import { createRoot } from 'react-dom/client';
{ createRoot } – Named import (notice the curly braces)
react-dom/client – ReactDOM library for browser/DOM manipulation
createRoot – New React 18+ API for rendering components to DOM
Line 4:
import App from './components/App';
App – Default import (no curly braces)
./components/App – Relative path to our App component
Note: We don’t need .jsx extension, esbuild figures it out
createRoot(container) – Create a React “root” at the DOM element
root.render(<App />) – Render our App component inside the container
<App /> – JSX syntax for using our component (self-closing tag)
🎯 Key React Concepts You Just Learned:
1. Components
Functions that return JSX
Must start with capital letter
Reusable pieces of UI
2. JSX
Looks like HTML, actually JavaScript
Must return single parent element
Processed by esbuild into regular JavaScript
3. Import/Export
Default exports:export default App → import App from './App'
Named exports:export { createRoot } → import { createRoot } from 'package'
4. React DOM
createRoot() – Modern way to mount React apps (React 18+)
render() – Display components in the browser
5. Rails Integration
Rails serves the HTML page
React takes over the #react-root element
esbuild bundles everything together
🚀 This pattern is the foundation of every React app! We create components, import them, and render them to the DOM.
📚 Step-by-Step React Learning with Todo List
Now let’s build a Todo List app step by step. I’ll explain each React concept thoroughly as we go. Here’s our learning roadmap:
Step 1: Understanding JSX and Basic Component Structure
First, let’s update our App.jsx to create the basic structure of our Todo app:
import React from 'react';
function App() {
return (
<div className="todo-app">
<h1>My Todo List</h1>
<p>Let's learn React by building a todo app!</p>
{/* This is a JSX comment */}
<div className="todo-container">
<h2>Add a new todo</h2>
<input type="text" placeholder="Enter a todo..." />
<button>Add Todo</button>
<h2>My Todos</h2>
<ul>
<li>Learn React basics</li>
<li>Build a todo app</li>
<li>Master React hooks</li>
</ul>
</div>
</div>
);
}
export default App;
🎯 Key Concepts Explained:
JSX (JavaScript XML):
JSX lets you write HTML-like syntax directly in JavaScript
It’s a syntax extension for JavaScript, not actual HTML
JSX gets compiled to JavaScript function calls
You can use {} to embed JavaScript expressions inside JSX
Important JSX Rules:
Use className instead of class (because class is a reserved word in JavaScript)
You can use single quotes for className values in JSX. Both work perfectly fine:
// Both of these are valid:
<div className='todo-app'> // Single quotes ✅
<div className="todo-app"> // Double quotes ✅
Quote Usage in JSX/JavaScript:
Single quotes vs Double quotes:
JavaScript treats them identically
It’s mostly a matter of personal/team preference
The key is to be consistent throughout your project
Common conventions:
// Option 1: Single quotes for JSX attributes
<div className='todo-app'>
<input type='text' placeholder='Enter todo...' />
</div>
// Option 2: Double quotes for JSX attributes
<div className="todo-app">
<input type="text" placeholder="Enter todo..." />
</div>
// Option 3: Mixed (but stay consistent within each context)
const message = 'Hello World'; // Single for JS strings
<div className="todo-app"> // Double for JSX attributes
When you MUST use specific quotes:
// When the string contains the same quote type
<div className="It's a great day"> // Double quotes needed
<div className='He said "Hello"'> // Single quotes needed
// Or use escape characters
<div className='It\'s a great day'> // Escaping single quote
<div className="He said \"Hello\""> // Escaping double quote
💡 Tip: Many teams use tools like Prettier or ESLint to automatically format and enforce consistent quote usage across the entire project.
All tags must be closed (self-closing tags need / at the end)
JSX comments use {/* */} syntax
Return a single parent element (or use React Fragment <>...</>)
Try updating our App.jsx with this code and see it in your browser!
Step 2: Introduction to State with useState
Now let’s add state to make our app interactive. State is data that can change over time.
import React, { useState } from 'react';
function App() {
// useState Hook - creates state variable and setter function
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React basics', completed: false },
{ id: 2, text: 'Build a todo app', completed: false },
{ id: 3, text: 'Master React hooks', completed: true }
]);
const [inputValue, setInputValue] = useState('');
return (
<div className="todo-app">
<h1>My Todo List</h1>
<div className="todo-container">
<h2>Add a new todo</h2>
<input
type="text"
placeholder="Enter a todo..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<button>Add Todo</button>
<h2>My Todos ({todos.length})</h2>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text} {todo.completed ? '✅' : '⏳'}
</li>
))}
</ul>
</div>
</div>
);
}
export default App;
🎯 Key Concepts Explained:
useState Hook:
useState is a React Hook that lets you add state to functional components
It returns an array with two elements: [currentValue, setterFunction]
const [todos, setTodos] = useState([]) creates a state variable todos and a function setTodos to update it
The initial value is passed as an argument to useState
Controlled Components:
The input field is now “controlled” by React state
value={inputValue} makes the input show what’s in state
onChange={(e) => setInputValue(e.target.value)} updates state when user types
Array.map() for Rendering Lists:
todos.map() transforms each todo into a JSX element
Each list item needs a unique key prop for React’s optimization
{todo.text} embeds the todo text using JSX expressions
Try this code and notice how the input field now responds to typing!
Step 3: Event Handling and Adding Todos
Let’s make the “Add Todo” button work:
import React, { useState } from 'react';
function App() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React basics', completed: false },
{ id: 2, text: 'Build a todo app', completed: false },
{ id: 3, text: 'Master React hooks', completed: true }
]);
const [inputValue, setInputValue] = useState('');
// Function to add a new todo
const addTodo = () => {
if (inputValue.trim() !== '') {
const newTodo = {
id: Date.now(), // Simple ID generation
text: inputValue,
completed: false
};
setTodos([...todos, newTodo]); // Spread operator to add new todo
setInputValue(''); // Clear the input field
}
};
// Function to handle Enter key press
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
addTodo();
}
};
return (
<div className="todo-app">
<h1>My Todo List</h1>
<div className="todo-container">
<h2>Add a new todo</h2>
<input
type="text"
placeholder="Enter a todo..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
/>
<button onClick={addTodo}>Add Todo</button>
<h2>My Todos ({todos.length})</h2>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text} {todo.completed ? '✅' : '⏳'}
</li>
))}
</ul>
</div>
</div>
);
}
export default App;
🎯 Key Concepts Explained:
Event Handlers:
onClick={addTodo} – function runs when button is clicked
onKeyPress={handleKeyPress} – function runs when key is pressed
Event handlers receive an event object (e) with information about the event
State Updates:
setTodos([...todos, newTodo]) – creates a new array with all existing todos plus the new one
Important: Always create new arrays/objects instead of mutating existing ones
React compares old and new state to determine if re-render is needed
Spread Operator (...):
...todos spreads out all elements of the todos array
This is the React way to add items to an array in state
Try adding new todos now!
🎯 Function Syntax Options in JavaScript:
const addTodo = () => {} is a function syntax – specifically an arrow function. Let me explain why it’s declared as const and the different ways to write functions in JavaScript.
1. Function Declaration (Traditional)
function addTodo() {
// function body
}
2. Function Expression with Arrow Function
const addTodo = () => {
// function body
};
3. Function Expression (Traditional)
const addTodo = function() {
// function body
};
🤔 Why use const for functions?
Arrow functions are expressions, not declarations:
// This is a DECLARATION - creates a function named addTodo
function addTodo() { }
// This is an EXPRESSION - creates a function and assigns it to a variable
const addTodo = () => { };
Why const specifically?
// ❌ Could be reassigned accidentally
let addTodo = () => { };
addTodo = "oops"; // Function is now gone!
// ❌ Could be reassigned accidentally
var addTodo = () => { };
addTodo = null; // Function is now gone!
// ✅ Cannot be reassigned - prevents bugs
const addTodo = () => { };
addTodo = "something"; // ERROR: Assignment to constant variable
📚 Key Differences:
Function Declaration vs Arrow Function:
// Function Declaration
function addTodo() {
console.log("Adding todo");
}
// Arrow Function (assigned to const)
const addTodo = () => {
console.log("Adding todo");
};
Hoisting Behavior:
// ✅ This works - function declarations are "hoisted"
sayHello(); // "Hello!"
function sayHello() {
console.log("Hello!");
}
// ❌ This doesn't work - arrow functions are not hoisted
sayGoodbye(); // Error: Cannot access 'sayGoodbye' before initialization
const sayGoodbye = () => {
console.log("Goodbye!");
};
this Binding:
// Function declaration has its own 'this'
function regularFunction() {
console.log(this); // 'this' can change based on how it's called
}
// Arrow function inherits 'this' from surrounding scope
const arrowFunction = () => {
console.log(this); // 'this' is inherited from parent scope
};
🚀 In React Context:
In React functional components, we typically use arrow functions with const because:
Prevents accidental reassignment – our function won’t get overwritten
Consistent with modern JavaScript – ES6+ standard
Cleaner syntax – less verbose than traditional function expressions
Better for event handlers – this behavior is more predictable
All these are equivalent in React:
// Option 1: Arrow function with const (most common)
const addTodo = () => {
if (inputValue.trim() !== '') {
// ... logic
}
};
// Option 2: Traditional function declaration
function addTodo() {
if (inputValue.trim() !== '') {
// ... logic
}
}
// Option 3: Function expression with const
const addTodo = function() {
if (inputValue.trim() !== '') {
// ... logic
}
};
No hoisting confusion – functions are defined before they’re used
Better with modern tooling – ESLint, Prettier handle them well
So yes, const addTodo = () => {} is definitely a function! It’s just a modern way to write functions that prevents accidental reassignment and has cleaner syntax.
🎯 What is Hoisting?
Hoisting is a fundamental JavaScript concept that can be confusing at first. Let me explain it clearly with examples.
Hoisting is JavaScript’s behavior of moving declarations to the top of their scope during the compilation phase, before the code is executed.
Think of it like JavaScript “hoists” (lifts up) your variable and function declarations to the top of their scope.
📚 How Hoisting Works:
Function Declarations are Hoisted:
// This works even though we call the function before declaring it!
sayHello(); // Outputs: "Hello!"
function sayHello() {
console.log("Hello!");
}
Behind the scenes, JavaScript treats it like this:
// JavaScript internally reorganizes it like this:
function sayHello() {
console.log("Hello!");
}
sayHello(); // Now it makes sense!
Variable Declarations (with var) are Hoisted:
console.log(myVar); // Outputs: undefined (not an error!)
var myVar = "Hello";
console.log(myVar); // Outputs: "Hello"
Behind the scenes:
// JavaScript internally treats it like this:
var myVar; // Declaration is hoisted to the top
console.log(myVar); // undefined (declared but not assigned)
myVar = "Hello"; // Assignment stays in place
console.log(myVar); // "Hello"
🚫 What is NOT Hoisted:
let and const variables:
// ❌ This throws an error!
console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
let myLet = "Hello";
// ❌ This also throws an error!
console.log(myConst); // ReferenceError: Cannot access 'myConst' before initialization
const myConst = "Hello";
Arrow Functions (Function Expressions):
// ❌ This throws an error!
sayGoodbye(); // ReferenceError: Cannot access 'sayGoodbye' before initialization
const sayGoodbye = () => {
console.log("Goodbye!");
};
Function Expressions:
// ❌ This throws an error!
myFunc(); // TypeError: myFunc is not a function
var myFunc = function() {
console.log("Hello!");
};
🎭 Visual Examples:
Example 1: Function Declaration Hoisting
// What you write:
console.log("1. Start");
myFunction(); // This works!
console.log("3. End");
function myFunction() {
console.log("2. Inside function");
}
// Output:
// 1. Start
// 2. Inside function
// 3. End
Example 2: Variable Hoisting with var
// What you write:
console.log("Value is:", myVar); // undefined
var myVar = 42;
console.log("Value is:", myVar); // 42
// What JavaScript internally does:
var myVar; // Hoisted to top
console.log("Value is:", myVar); // undefined
myVar = 42; // Assignment stays here
console.log("Value is:", myVar); // 42
Example 3: Why let and const are different
// Temporal Dead Zone
console.log("Before declaration");
// ❌ This area is the "Temporal Dead Zone" for myLet
console.log(myLet); // ReferenceError!
let myLet = "Hello"; // Declaration and initialization
console.log(myLet); // "Hello"
🎯 Practical Implications:
Why Function Declarations work everywhere:
// This is valid JavaScript
function main() {
helper(); // Can call helper before it's defined
function helper() {
console.log("Helper function");
}
}
Why Arrow Functions must be defined first:
// ❌ This doesn't work
const main = () => {
helper(); // Error! helper is not defined yet
const helper = () => {
console.log("Helper function");
};
};
// ✅ This works
const main = () => {
const helper = () => {
console.log("Helper function");
};
helper(); // Now it works!
};
💡 Best Practices:
Use const and let instead of var to avoid hoisting confusion
Define functions before using them (even though function declarations are hoisted)
Use arrow functions for consistency and to avoid hoisting surprises
Initialize variables when you declare them
🔗 Back to Our React Example:
// This is why we use const for arrow functions
const App = () => {
// ✅ Good: Function defined before use
const addTodo = () => {
// function logic
};
// ❌ Bad: Would cause error if we tried to call it here
// deleteTodo(); // Error!
const deleteTodo = () => {
// function logic
};
return (
<div>
<button onClick={addTodo}>Add</button>
<button onClick={deleteTodo}>Delete</button>
</div>
);
};
Hoisting is JavaScript’s way of making function declarations available throughout their scope, but it doesn’t apply to modern variable declarations (let, const) or arrow functions. That’s why we define our functions with const and make sure to declare them before we use them!
Step 4: Toggling Todo Completion
Let’s add the ability to mark todos as complete/incomplete:
When working with asset pipelines in Ruby on Rails 7 and 8, you might encounter Sprockets and Propshaft—two asset handling libraries. While both aim to serve static assets like JavaScript, CSS, images, and fonts, they do so in different ways.
This post will walk you through what each does, how they differ, and when you might want to use one over the other.
📦 What is Sprockets?
Sprockets is the original Rails asset pipeline system, introduced way back in Rails 3.1. It allows developers to:
Concatenate and minify JavaScript and CSS
Preprocess assets using things like SCSS, CoffeeScript, ERB, etc.
Fingerprint assets for cache busting
Compile assets at deploy time
It works well for traditional Rails applications where the frontend and backend are tightly coupled.
Supports advanced directives like //= require_tree .
Cons:
Complex internal logic
Slower compilation times
Relies on a manifest file that can get messy
Tightly coupled with older Rails asset practices
🧵 What is Propshaft?
Propshaft is the newer asset pipeline introduced by the Rails team as an alternative to Sprockets. It focuses on simplicity and modern best practices. Propshaft was added as an optional asset pipeline starting in Rails 7 and is included by default in some new apps.
Design Philosophy: Propshaft aims to work like a static file server with fingerprinting and logical path mapping, rather than a full asset compiler.
And your app/assets/builds/application.css could be compiled via Tailwind or SCSS using a toolchain.
🧠 Final Thoughts
Sprockets has served Rails well for over a decade, but Propshaft is the new lightweight future. If you’re starting fresh, Propshaft is a strong choice, especially when used alongside Hotwire, Importmaps, or modern JS bundlers.
However, don’t feel pressured to switch if your current Sprockets setup works fine—Rails continues to support both.
✨ TL;DR
Sprockets = older, feature-rich, best for legacy apps
Propshaft = newer, minimal, better for modern workflows
Choose based on your app’s needs and complexity. Cheers! 🚀
Now let’s create an admin interface for our e-commerce Application.
We have a well-structured e-commerce Rails application with:
Models: User, Product, ProductVariant, Order, OrderItem Authentication: Custom session-based auth with user roles (customer/admin) Authorization: Already has admin role checking
Admin Interface Recommendations
Here are the best options for Rails admin interfaces, ranked by suitability for our project:
ActiveAdmin (Recommended ⭐) Best fit for e-commerce with complex associations Excellent filtering, search, and batch operations Great customization options and ecosystem Handles your Product → ProductVariant → OrderItem relationships well
Administrate (Modern Alternative) Clean, Rails-way approach by Thoughtbot Good for custom UIs, less configuration More work to set up initially
Rails Admin (What you asked about) Quick setup but limited customization Less actively maintained Good for simple admin needs
Choose ActiveAdmin for our e-commerce application. Let’s integrate it with our existing authentication system
Add in Gemfile:
gem "activeadmin"
gem "sassc-rails" # Required for ActiveAdmin
gem "image_processing", "~> 1.2" # For variant processing if not already present
Bundle Install and run the Active Admin Generator:
$ bundle install
$ rails generate active_admin:install --skip-users
definition of Rules was here
create app/assets/javascripts/active_admin.js
create app/assets/stylesheets/active_admin.scss
create db/migrate/20250710083516_create_active_admin_comments.rb
Migration File created by Active Admin:
class CreateActiveAdminComments < ActiveRecord::Migration[8.0]
def self.up
create_table :active_admin_comments do |t|
t.string :namespace
t.text :body
t.references :resource, polymorphic: true
t.references :author, polymorphic: true
t.timestamps
end
add_index :active_admin_comments, [ :namespace ]
end
def self.down
drop_table :active_admin_comments
end
end
Run database migration:
$ rails db:migrate
in app/initializers/active_admin.rb
# This setting changes the method which Active Admin calls
# within the application controller.
config.authentication_method = :authenticate_admin_user!
....
# This setting changes the method which Active Admin calls
# (within the application controller) to return the currently logged in user.
config.current_user_method = :current_admin_user
....
# Default:
config.logout_link_path = :destroy_session_path
in app/controllers/application_controller.rb
private
def authenticate_admin_user!
require_authentication
ensure_admin
end
def current_admin_user
Current.user if Current.user&.admin?
end
Run the active admin user, product generator:
rails generate active_admin:resource User
rails generate active_admin:resource Product
rails generate active_admin:resource ProductVariant
rails generate active_admin:resource Order
rails generate active_admin:resource OrderItem
Let’s update all the active admin resources with fields, filters, attributes, panels etc.
Let’s add accepts_nested_attributes_for :variants, allow_destroy: true in Product Model.
accepts_nested_attributes_for is a Rails feature that allows a parent model to accept and process attributes for its associated child models through nested parameters. Here’s what it does:
What it enables:
Nested Forms: You can create/update a Product and its ProductVariants in a single form submission
Mass Assignment: Allows passing nested attributes through strong parameters
CRUD Operations: Create, update, and delete associated records through the parent
In our Product model
class Product < ApplicationRecord
has_many :variants, dependent: :destroy, class_name: "ProductVariant"
accepts_nested_attributes_for :variants, allow_destroy: true
end
What this allows:
Before: You’d need separate forms/requests for Product and ProductVariant
This enables the nested form I created in app/admin/products.rb:
f.inputs "Product Variants" do
f.has_many :variants, allow_destroy: true, new_record: true do |v|
v.input :size
v.input :color
v.input :sku
# ... other variant fields
end
end
The allow_destroy: true option:
Allows deleting associated records by passing _destroy: ‘1’
Essential for updating existing products with variants
Strong Parameters:
In the controller, you need to permit the nested attributes:
Rendered layout layouts/application.html.erb (Duration: 13.9ms | GC: 0.5ms)
Completed 500 Internal Server Error in 118ms (ActiveRecord: 28.7ms (1 query, 0 cached) | GC: 27.0ms)
ActionView::Template::Error (undefined method 'load_path' for an instance of Sprockets::Environment)
Caused by: NoMethodError (undefined method 'load_path' for an instance of Sprockets::Environment)
Information for: ActionView::Template::Error (undefined method 'load_path' for an instance of Sprockets::Environment):
14: <link rel="icon" href="/icon.svg" type="image/svg+xml">
15: <link rel="apple-touch-icon" href="/icon.png">
This error occurs because Rails 8 uses Propshaft by default, but ActiveAdmin requires Sprockets. There’s a conflict between the two asset pipelines. Let’s fix this by switching to Sprockets completely:
Add in Gemfile:
# Use Sprockets for asset pipeline (required for ActiveAdmin)
gem "sprockets-rails"
$ bundle install
# application.rb
# Use Sprockets for asset pipeline
config.assets.enabled = true
Let’s create an admin user for testing our ActiveAdmin:
# seed.rb
# Create admin user
admin_user = User.find_or_create_by(email: "admin@designstudio.com") do |user|
user.password = "password123"
user.role = "admin"
end
puts "Admin user created: #{admin_user.email}" if admin_user.persisted?
# run seed
✗ rails db:seed
✅ We have Successfully Integrated ActiveAdmin with Nested Attributes!
I’ve successfully converted your Rails application to use nested attributes (variants_attributes) across the entire stack and integrated ActiveAdmin as your admin interface. Here’s what I accomplished:
🔧 What I Fixed & Updated:
1. Asset Pipeline Issue:
Switched from Propshaft to Sprockets (required for ActiveAdmin)
Updated Gemfile and application configuration
Fixed the load_path error you encountered
2. Model Changes:
Added accepts_nested_attributes_for :variants, allow_destroy: true to Product model
3. Controller Updates:
Changed from singular product_variant to plural variants_attributes
Simplified create/update actions to use Rails nested attributes handling
Updated strong parameters to permit nested variant attributes
4. View Improvements:
Updated product form to handle multiple variants with JavaScript
Added “Add Another Variant” functionality
Implemented proper nested form fields for all variant attributes
5. ActiveAdmin Configuration:
Set up authentication using your existing user system
Created comprehensive admin resources for all models
The issue is with <%= stylesheet_link_tag "/tailwind/application.css", "data-turbo-track": "reload" %> where we are trying to load Tailwind CSS from an incorrect path. The tailwindcss-rails gem serves Tailwind CSS differently. Let me fix this:
The issue is that the /tailwind/application.css path is returning 404. The tailwindcss-rails gem works differently. Let’s check how it’s configured and fix this:
find . -name "*.css" -path "*/public/*" | head -10
rails assets:precompile
Now let’s test if the Tailwind CSS is properly accessible:
The issue is that with the tailwindcss-rails gem, Tailwind CSS gets compiled into the main application.css file rather than a separate file. Let’s check the current asset manifest and fix the layout:
rails assets:precompile
bundle exec tailwindcss build
The Tailwind CSS is already built and located in app/assets/builds/tailwind.css. Let’s fix the configuration:
rails assets:precompile
curl -I http://localhost:3000/assets/tailwind.css
The issue has been fixed. The Tailwind CSS is now properly accessible. Here’s what I did to resolve the problem:
Identified the issue: The Tailwind CSS was being incorrectly referenced as /tailwind/application.css in the layout file, but it wasn’t being served properly through the asset pipeline.
Found the correct location: Discovered that the tailwindcss-rails gem builds the CSS file to app/assets/builds/tailwind.css.
Fixed the asset pipeline configuration:
Updated app/assets/config/manifest.js to include //= link_tree ../builds which includes the built Tailwind CSS
Removed the incorrect tailwind/application.css reference
Updated the layout file:
Changed from <%= stylesheet_link_tag "/tailwind/application.css" %>
To the correct <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
Verified the fix:
Successfully precompiled assets
Confirmed that http://localhost:3000/assets/tailwind.css returns a 200 OK status
Install Rails Icons 🍁
For better font-awesome assets, lets add rails-icons gem:
✅ Rails Icons is now properly set up instead of the CDN. Here’s what we’ve accomplished:
What’s Changed:
✅ Removed the Font Awesome CDN link (no more external dependency)
✅ Addedrails_icons gem with Heroicons as the default library
✅ Added Font Awesome as a custom library with all variants (solid, regular, brands)
✅ Self-hosted all icons through Rails asset pipeline
What is Ransack?
Ransack is a popular Ruby gem that provides powerful search and filtering capabilities for Rails applications. It’s commonly used with ActiveAdmin (which your app uses) to create searchable, filterable, and sortable data tables.
Ransackable Attributes and Associations
ransackable_attributes
This method defines which model attributes can be searched, filtered, or sorted through Ransack. It’s a security feature that prevents unauthorized access to sensitive data.
def self.ransackable_attributes(auth_object = nil)
%w[id email role created_at updated_at password_reset_token password_reset_sent_at]
end
What it does:
Allows searching/filtering by id, email, role, created_at, updated_at, etc.
Prevents searching by sensitive fields like password_digest
The auth_object parameter can be used for role-based access control
ransackable_associations
This method defines which model associations can be used in Ransack queries.
def self.ransackable_associations(auth_object = nil)
%w[orders sessions]
end
What it does:
Allows searching/filtering by related models (e.g., “users who have orders”)
Enables joins and complex queries across associations
Why This Matters for Security
Without these methods, Rails 7+ will raise warnings or errors because Ransack needs explicit permission to search certain fields. This prevents:
Information disclosure – Searching sensitive fields like passwords
Performance issues – Searching on unindexed or inappropriate fields
Unauthorized access – Accessing data through unexpected query parameters
Example Usage in ActiveAdmin
In your ActiveAdmin dashboard, this enables features like:
# In app/admin/users.rb
ActiveAdmin.register User do
# Users can now search by email, role, etc.
# Users can filter by orders, sessions
# Users can sort by created_at, updated_at
end
In Our User Model
Looking at your User model:
def self.ransackable_attributes(auth_object = nil)
%w[id email role created_at updated_at password_reset_token password_reset_sent_at]
end
def self.ransackable_associations(auth_object = nil)
%w[orders sessions]
end
This means:
✅ Admins can search users by email, role, creation date
✅ Admins can filter users by their orders or sessions
✅ Admins can sort users by any of these attributes
❌ Admins cannot search by password_digest (secure)
❌ Admins cannot search by other sensitive fields
Benefits
Security: Explicit control over what can be searched
Performance: Only searchable fields need indexes
Usability: Provides powerful admin interface features
Compliance: Helps meet data protection requirements
Our application should work exactly the same now, but with better performance and no external dependencies! You can gradually migrate icons from <i class="fas fa-x"> to <%= icon "x", library: "fontawesome", variant: "solid" %> as needed.
Asset Issue Again
✅ Final Fix Applied:
Added //= link_tree ../../javascript .js to the manifest, which tells Sprockets to include all JavaScript files from the app/javascript directory tree.
✅ What’s Working Now:
All CSS files properly compiled:
tailwind.css ✅
application.css ✅
active_admin.css ✅
All JavaScript files properly compiled:
application.js ✅
controllers/application.js ✅
controllers/decorative_background_controller.js ✅
controllers/hello_controller.js ✅
controllers/index.js ✅
Asset Pipeline fully functional:
Sprockets serving all assets correctly
Proper cache headers and ETags
All asset links declared in manifest
🎯 Current Status:
Our Rails application now has:
✅ Modern asset pipeline with Sprockets + Importmap
✅ Tailwind CSS for styling (self-hosted)
✅ ActiveAdmin for administration
✅ Rails Icons instead of Font Awesome CDN
✅ Stimulus controllers for JavaScript functionality