Rails Overview & Philosophy
Ruby on Rails is a model–view–controller (MVC) framework, providing default structures
for a database, a web service, and web pages. It encourages and facilitates the use of
web standards such as JSON or XML for data transfer and HTML, CSS, and JavaScript for
user interfacing. The framework is designed to make programming web applications easier
by making assumptions about what every developer needs to get started. It allows you to
write less code while accomplishing more than many other languages and frameworks.
The philosophy of Rails is guided by two primary principles:Don't Repeat Yourself
(DRY)
and Convention over Configuration (CoC). DRY is a principle of software
development
which states that "Every piece of knowledge must have a single, unambiguous,
authoritative representation within a system." By not writing the same information over
and over again, the code is more maintainable, more extensible, and less buggy. CoC
means that Rails has opinions about the best way to do many things in a web application.
Instead of requiring you to configure every minute detail through endless XML or YAML
files, Rails follows conventions that allow it to "just work" if you follow the
established naming and directory structures.
Installation & Setup
Before installing Rails, you must ensure that your environment has a compatible version
of
the Ruby programming language, a database engine (such as SQLite, PostgreSQL, or MySQL),
and
a package manager for JavaScript dependencies (typically Yarn or npm). Rails uses
RubyGems
to manage its library dependencies. The installation process generally begins with
updating
the local system dependencies and then installing the Rails gem itself.
Warning: Always ensure you are using a Ruby version manager
like rbenv or rvm. Using the
system Ruby provided by your OS can lead to permission issues and conflicts during gem
updates.
| Dependency |
Purpose |
Minimum Version (Recommended) |
| Ruby |
The underlying programming language. |
3.2.0+ |
| SQLite3 |
Default lightweight database for development. |
3.22.0+ |
| Node.js |
Required for JavaScript asset compilation. |
18.x+ |
| Yarn |
Manages frontend packages and dependencies. |
1.22+ |
To install Rails, you execute the following command in your terminal:
# Update RubyGems and install the Rails gem
gem update --system
gem install rails
After the installation completes, verify the installation by checking the Rails version
to
ensure the binary is correctly mapped in your path.
rails --version
# Output: Rails 7.1.x (or current stable version)
Creating a New Rails Project
Generating a new Rails application is handled by the rails new command-line
tool. This
command creates a directory structure that contains all the files and folders necessary
to
run a Rails application. When you run this command, Rails initializes a Git repository,
runs
bundle install to fetch required gems, and configures the default
application
settings.
The command accepts several flags that allow you to customize the initial stack, such as
choosing a specific database or skipping certain frameworks like Action Mailer or Action
Cable if they are not required for your project.
| Argument/Flag |
Description |
Example |
| app_name |
The name of the directory and the application module. |
my_app |
| --database |
Specifies the database engine to be used. |
-d postgresql |
| --javascript |
Configures the JS bundling approach (importmap, esbuild). |
-j esbuild |
| --css |
Sets the CSS processor (tailwind, bootstrap, sass). |
-c tailwind |
To create a standard application using the default SQLite database, use:
rails new my_api_project
cd my_api_project
Once inside the directory, you can start the built-in Puma web server to confirm the
scaffolding was successful.
bin/rails server
Hello World (MVC Basic Flow)
The "Hello World" implementation in Rails serves as the primary introduction to the
Model-View-Controller (MVC) architectural pattern. In Rails, a request
is
first intercepted
by the Router, which determines which Controller
should
handle the logic. The Controller
then interacts with the Model (if data is needed) and finally renders
a View the user.
To implement a basic "Hello World," you must first define a route in
config/routes.rb. This
tells Rails that a specific URL pattern should map to a specific action inside a
controller.
# config/routes.rb
Rails.application.routes.draw do
root "pages#hello"
end
Next, the controller must be created. The controller is a Ruby class that inherits from
ApplicationController. Inside this class, you define methods (actions) that
correspond to
the routes.
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
def hello
@message = "Hello, Rails!"
end
end
Finally, the View provides the HTML structure. Rails uses Embedded Ruby
(ERB) files, which
allow you to mix standard HTML with Ruby code. By convention, the view file must be
located
in a folder named after the controller and a file named after the action.
<%# app/views/pages/hello.html.erb %>
<h1>Greeting Page</h1>
<p><%= @message %></p>
Note
Instance variables defined in the controller action (starting with
@)
are automatically made available to the corresponding view template. Local
variables
(without @) are not shared with the view.
Active Record Basics
Active Record is the Model layer of the MVC pattern in Rails. It is an implementation of
the
Active Record pattern, which is a technique for managing data in relational databases.
In
Rails, Active Record serves as an Object-Relational Mapping (ORM) system. This means it
maps
the tables in your database to Ruby classes (Models) and the rows in those tables to
objects
(Instances) of those classes. By using Active Record, developers can interact with the
database using pure Ruby code, abstracting away the complexities of writing raw SQL for
common Create, Read, Update, and Delete (CRUD) operations.
The power of Active Record lies in its ability to automatically sense the database
schema.
When a class inherits from ApplicationRecord, Rails introspects the
database
table
associated with that class and automatically creates accessor methods for every column.
For
example, if a users table has an email column, the
User model will automatically have email
and email= methods.
Naming Conventions
Active Record relies heavily on naming conventions to map models to database tables
without
explicit configuration. By default, Rails pluralizes the class name to find the
corresponding table name. If you violate these conventions, Rails provides methods to
manually override the table name or primary key, though this is generally discouraged.
| Entity |
Naming Convention |
Example |
| Model Class |
Singular, CamelCase |
UserContact |
| Database Table |
Plural, snake_case |
user_contacts |
| Primary Key |
Always named id |
id (Integer/UUID) |
| Foreign Key |
singular_table_name_id |
user_id |
CRUD Operations
Active Record provides a robust API for interacting with data. These operations are
handled
through class methods for fetching data and instance methods for modifying individual
records.
# CREATE
# Create and save a new record to the database
user = User.create(name: "John Doe", email: "john@example.com")
# READ
# Find the first user in the database
user = User.first
# Find a user by a specific attribute
user = User.find_by(email: "john@example.com")
# Find a user by their primary key
user = User.find(1)
# UPDATE
# Change an attribute and save it
user.update(name: "Jane Doe")
# DELETE
# Remove the record from the database
user.destroy
Database Migrations
In Rails, you do not modify the database schema by writing SQL manually. Instead, you use
Migrations. Migrations are a DSL (Domain Specific Language) for describing changes to
your
database over time in a consistent and repeatable way. They are stored as files in
db/migrate and allow team members to stay in sync with the current state of
the
database.
Note
Active Record Migrations are version-controlled. Each migration file is
timestamped,
ensuring that Rails executes them in the correct chronological order when you
run
bin/rails db:migrate.
# Example migration to create a products table
class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
t.string :name
t.text :description
t.decimal :price, precision: 8, scale: 2
t.boolean :is_published, default: false
t.timestamps # Automatically adds created_at and updated_at columns
end
end
end
Schema and Metadata
Active Record automatically manages two special columns via the t.timestamps
macro: created_at and updated_at.M These are automatically
populated by Rails when a record is first created or subsequently modified. Furthermore,
Rails maintains a db/schema.rb file, which is the authoritative source for
your
current database structure. You should never edit this file directly; it is
automatically
updated whenever you run migrations.
Warning: When performing bulk operations like update_all or
delete_all, Active Record bypasses model instantiations. This means that
validations and callbacks (like updating the updated_at timestamp) will
not be
triggered. Use these methods with caution.
Migrations (Database Changes)
Migrations are a robust feature of Active Record that allow developers to evolve the
database schema over time in a consistent and easy way. Rather than managing raw SQL
scripts, migrations use a Ruby DSL to describe changes to your tables. This approach
ensures that schema changes are platform-independent and can be shared across
multiple
development environments through version control.
Each migration represents a new "version" of the database. When you run a migration,
Rails tracks which versions have already been applied by storing the migration’s
unique
timestamp in a special metadata table called schema_migrations. This
prevents the same change from being applied twice and allows for easy "rolling back"
if
a change causes issues.
Anatomy of a Migration File
A migration file consists of a class that inherits from
ActiveRecord::Migration, tagged with the specific Rails version it was
created for (e.g., [7.1]). The primary method within this class is
change, which Rails uses to determine how to apply the migration and,
in
many cases, how to automatically reverse it.
class CreateArticles < ActiveRecord::Migration[7.1]
def change
create_table :articles do |t|
t.string :title
t.text :body
t.boolean :published, default: false
t.timestamps
end
end
end
In more complex scenarios where a migration is not automatically reversible (such as
removing a column or changing a data type), you must replace the change
method with
distinct up and down methods to explicitly define the
migration and rollback logic.
Common Migration Methods
Active Record provides a wide array of methods to transform your database. These
methods
translate directly into the appropriate SQL syntax for your specific database engine
(PostgreSQL, MySQL, or SQLite).
| Method |
Purpose |
Example |
create_table |
Creates a new table with a block for column definitions. |
create_table :users do |t| ... end |
add_column |
Adds a new column to an existing table. |
add_column :users, :email, :string |
remove_column |
Deletes a column from a table. |
remove_column :posts, :slug, :string |
rename_column |
Renames an existing column. |
rename_column :users, :pass, :password |
add_index |
Adds a database index for performance or uniqueness. |
add_index :users, :email, unique: true |
add_reference |
Creates a foreign key column (belongs_to relationship). |
add_reference :posts, :user, foreign_key: true |
Running and Reverting Migrations
The command-line interface for migrations is accessed via bin/rails.
These
commands allow you to move forward through pending migrations or return to a
previous
state.
# Apply all pending migrations
bin/rails db:migrate
# Roll back the very last migration performed
bin/rails db:rollback
# Roll back the last 3 migrations
bin/rails db:rollback STEP=3
# Check the status (up/down) of all migrations
bin/rails db:migrate:status
Warning: Never modify a migration file that has already been shared
and
run in a
production environment. Doing so creates a "schema mismatch" between environments.
If
you need to fix a mistake in an existing table, generate a new
migration to apply the
fix.
Supported Data Types
When defining columns within a migration, Active Record maps Ruby-like symbols to the
underlying database's native types.
| Type |
Ruby/Database Representation |
Use Case |
:string |
VARCHAR(255) |
Short text like names or titles. |
:text |
TEXT |
Long-form content like blog posts or comments. |
:integer |
INT |
Whole numbers, counts, or IDs. |
:decimal |
DECIMAL |
Financial data where precision is required. |
:datetime |
DATETIME / TIMESTAMP |
Precise points in time. |
:jsonb |
JSONB (PostgreSQL only) |
Structured, searchable document data. |
Note
The t.timestamps method is a macro that adds two columns: created_at and
updated_at. Rails handles the population of these fields automatically
whenever
a record is created or saved via Active Record.
Validations (Data Integrity)
Validations are used to ensure that only valid data is saved into your database. In
the Rails MVC hierarchy, validations occur at the Model level.
While database-level constraints are also important, model-level validations are
more flexible, easier to test, and provide a better user experience by allowing
Rails to gracefully handle errors and display them back to the user before the
database layer is even reached.
Validations are triggered whenever an object is saved to the database. By default,
Rails runs validations when you call save, create, or
update. If any validation fails, the object is marked as invalid, Rails
does not perform the SQL INSERT or UPDATE operation, and
the save method returns false.
Common Validation Helpers
Active Record offers many pre-defined validation helpers that cover common
requirements. These helpers are declared at the top of your model class and can
accept various options to customize their behavior.
| Helper |
Description |
Example Usage |
presence |
Ensures the specified attributes are not empty. |
validates :name, presence: true |
uniqueness |
Ensures the value is unique across the table. |
validates :email, uniqueness: true |
length |
Constrains the character count of an attribute. |
validates :bio, length: { maximum: 500 } |
numericality |
Ensures the attribute has only numeric values. |
validates :age, numericality: { only_integer: true }
|
format |
Validates value against a Regular Expression. |
validates :code, format: { with: /\A[A-Z]+\z/ }
|
inclusion |
Ensures the value is within a specific set/array. |
validates :size, inclusion: { in: %w(small medium large) }
|
Implementation Example
The following code demonstrates how to apply multiple validations to a
User model. Notice how we can combine multiple constraints into a
single line or separate them for clarity.
class User < ApplicationRecord
# Ensures name exists and is at least 2 characters
validates :name, presence: true, length: { minimum: 2 }
# Ensures email is present, unique, and matches a basic regex
validates :email,
presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
# Ensures age is a number and at least 18
validates :age, numericality: { greater_than_or_equal_to: 18 }
end
Working with Validation Errors
When a validation fails, Rails populates an errors collection on the
model instance. You can use this collection to debug why a record is invalid or to
display error messages in your frontend views.
user = User.new(name: "J")
if user.invalid?
puts user.errors.full_messages
# Output: ["Name is too short (minimum is 2 characters)", "Email can't be blank"]
end
# Checking specific field errors
user.errors[:email] # => ["can't be blank"]
Conditional Validations and Triggers
Sometimes a validation should only run under specific circumstances. You can use the
:if and :unless options, which take a symbol (referring to
a method), a string, or a Proc. Additionally, you can specify exactly when a
validation should run using the :on option.
| Option |
Purpose |
Example |
:on |
Specifies the action (create or update). |
validates :email, presence: true, on: :create |
:allow_nil |
Skips validation if the value is nil. |
validates :bio, length: { maximum: 100 }, allow_nil: true
|
:if |
Runs validation only if a condition is met. |
validates :card_number, presence: true, if: :paid_with_card?
|
Warning: Certain methods in Active Record bypass validations entirely. Methods like
save(validate: false),update_column,
update_all, and touch will persist data to the database
even if it violates your model's validation rules. Use these with extreme caution to
avoid data corruption.
Note
Note: For complex validation logic that cannot be handled by built-in
helpers, you can define "Custom Validators" by inheriting from
ActiveModel::Validator or by writing a private method within
your model and calling validate :your_method_name.
Callbacks (Hooks)
Callbacks are methods that get called at specific moments in the lifecycle of an
Active Record object. They allow you to trigger logic before or after a change
in the object's state, such as when it is created, updated, or destroyed. This
is particularly useful for automating tasks like data normalization, complex
logging, or updating associated records without cluttering the controller logic.
By hooking into the lifecycle, you ensure that specific business rules are
enforced every time a model instance is persisted, regardless of where the call
originates in your application.
The Callback Lifecycle
The Active Record lifecycle is highly granular. Callbacks are executed in a
specific order surrounding the database operation. If any "before" callback
throws an error or explicitly halts the process, the entire transaction is
rolled back, and the operation (save, update, or destroy) returns
false.
| Category |
Available Hooks |
| Object Creation |
before_validation,
after_validation, before_save,
around_save, before_create,
around_create, after_create,
after_save, after_commit
|
| Object Update |
before_validation,
after_validation, before_save,
around_save, before_update,
around_update, after_update,
after_save, after_commit
|
| Object Destruction |
before_destroy, around_destroy,
after_destroy, after_commit
|
Implementation Example
The most common use for callbacks is data preparation. In the example below, we
ensure that an email address is downcased before saving it to the database to
prevent duplicate accounts with varying cases, and we trigger a notification
after a record is successfully written to the disk.
class User < ApplicationRecord
# Normalize data before validation
before_validation :normalize_email
# Trigger logic only after the database transaction is finalized
after_commit :send_welcome_email, on: :create
private
def normalize_email
self.email = email.downcase.strip if email.present?
end
def send_welcome_email
UserMailer.welcome_email(self).deliver_later
end
end
Halting Execution
In Rails 5 and later, callbacks no longer halt if they return false.
To stop the lifecycle (for instance, to prevent a record from being deleted),
you must explicitly throw an abort signal. This is a critical safety mechanism
when dealing with sensitive data.
class Order < ApplicationRecord
before_destroy :ensure_not_shipped
private
def ensure_not_shipped
if shipped?
errors.add(:base, "Cannot delete a shipped order")
throw(:abort) # This is the correct way to halt the callback chain
end
end
end
Transactional Callbacks
There are two special callbacks that run outside of the standard database
transaction: after_commit and after_rollback. These
are essential when you need to interact with external systems (like sending an
email, clearing a cache, or hitting an API).
Using a standard after_save for these tasks is risky because the
after_save runs inside the transaction. If the transaction fails
after the code executes, your external service will have been triggered for data
that doesn't actually exist in your database.
| Callback |
Purpose |
Best Use Case |
after_commit |
Runs after the DB transaction is finalized. |
Sending emails or background job queuing. |
after_rollback |
Runs if the DB transaction fails. |
Cleaning up temporary files or logging failures. |
after_initialize |
Runs whenever an object is instantiated. |
Setting default values for new objects. |
after_find |
Runs when Rails loads a record from the DB. |
Decrypting sensitive data for use in Ruby. |
Warning: Be cautious of "Callback Hell." Overusing callbacks can
make models difficult to test and debug, as logic may trigger unexpectedly. If a
callback involves complex logic that isn't strictly related to data integrity,
consider moving it to a Service Object.
Note
Just like validations, certain methods bypass callbacks.
update_column, update_all,
delete, and delete_all will perform database
operations without triggering any defined hooks.
Associations (Relations between Models)
In Rails, associations are connections between two Active Record models. They
make common operations simpler and easier to write by allowing you to
declare the relationship between models in a declarative way. Without
associations, you would need to manually manage foreign keys and write
complex finders to retrieve related data. By declaring associations, Rails
handles the "heavy lifting" of the underlying SQL joins and foreign key
assignments.
Associations tell Rails that two models have a connection, and they provide a
suite of methods to the model instances to interact with that connection.
For example, if a User has many Posts, declaring
this association allows you to call user.posts to retrieve an
array of post objects rather than writing a manual SQL query.
Types of Associations
Rails supports several types of associations, each representing a different
kind of relationship in the database schema. Choosing the correct
association depends on where the foreign key is located and the nature of
the relationship (singular vs. plural).
| Association |
Description |
Foreign Key Location |
belongs_to |
Sets up a one-to-one connection with another model. |
The model declaring it. |
has_one |
Sets up a one-to-one connection where the other model
has the key. |
The associated model. |
has_many |
A one-to-many relationship. |
The associated model. |
has_many :through |
A many-to-many relationship using a join model. |
The join table. |
has_and_belongs_to_many |
A many-to-many relationship without a join model. |
A separate join table. |
One-to-Many: The Most Common Relationship
In a one-to-many relationship, one model (the parent) can have many instances
of another model (the child), while each child belongs to exactly one
parent.
# app/models/author.rb
class Author < ApplicationRecord
has_many :books,? dependent: :destroy
end
# app/models/book.rb
class Book < ApplicationRecord
belongs_to :author
end
With this setup, Rails automatically provides methods to manage the
relationship:
author.books (returns a collection)
author.books << book (adds a book and saves it)
book.author (returns the author object)
Many-to-Many:has_many :through
When two models need to share many instances of each other, a join model is
recommended. This approach is superior to
has_and_belongs_to_many because it allows you to store
additional metadata on the relationship itself (e.g., the date a user joined
a group).
# app/models/physician.rb
class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
# app/models/appointment.rb (The Join Model)
class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
# app/models/patient.rb
class Patient < ApplicationRecord
has_many :appointments
has_many :physicians, through: :appointments
end
Polymorphic Associations
Polymorphic associations allow a model to belong to more than one other model
on a single association. For example, you might have a Comment
model that can belong to both aPost and a Video.
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
# app/models/post.rb
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
# app/models/video.rb
class Video < ApplicationRecord
has_many :comments, as: :commentable
end
Warning: When using has_many, be mindful of the
:dependent option. If you
delete a parent record, associated child records might become "orphaned" in
the database. Use dependent: :destroy to ensure associated
records are also
removed, or dependent: :nullify to set their foreign keys to
NULL.
Important Options
Associations can be customized using various options to change their
behavior.
| Option |
Purpose |
class_name |
Specifies the actual class name if it differs from the
association name. |
foreign_key |
Manually specifies the foreign key column name. |
dependent |
Defines what happens to associated records when the
owner is destroyed. |
validate |
If true, validates associated objects whenever the owner
is saved. |
Note
Active Record associations use Lazy Loading. If you
call Author.all.each { |a| puts a.books.count }, Rails
will execute one query for the authors and one additional query for
each author's books. To avoid this "N+1 query" performance
bottleneck, always use includes
(e.g., Author.includes(:books).all).
Query Interface (Finding Data)
The Active Record Query Interface is a powerful DSL that allows you to
retrieve data from your database using Ruby methods instead of writing
raw SQL. It is designed to be highly readable and chainable, meaning you
can combine multiple methods to build complex queries dynamically. Under
the hood, Active Record generates highly optimized SQL specific to your
database engine (PostgreSQL, MySQL, or SQLite).
One of the most important aspects of the query interface isLazy
Loading (or Method Chaining). When you call a method like
where or order, Active Record does not
immediately execute the query. Instead, it returns an
ActiveRecord::Relation object. The SQL query is only
executed when the data is actually needed (e.g., when you iterate over
the results or call a method like .to_a).
Retrieving Single Objects
When you need to find a specific record, Rails provides several methods
that differ in how they handle missing data and which columns they use
for the search.
| Method |
Description |
Behavior if Not Found |
find(id) |
Finds a record by its primary key. |
Raises ActiveRecord::RecordNotFound
|
find_by(...) |
Finds the first record matching the criteria. |
Returns nil |
first |
Returns the first record (ordered by primary key).
|
Returns nil |
last |
Returns the last record (ordered by primary key).
|
Returns nil |
find_or_create_by |
Finds a record or creates it if it doesn't exist.
|
Returns the found or new object |
# Finding by ID
user = User.find(1)
# Finding by specific attributes
user = User.find_by(email: 'test@example.com', active: true)
# Using find_by! to raise an error if not found
user = User.find_by!(username: 'gemini_user')
Filtering and Ordering
To retrieve collections of records, you use methods that define the
WHERE, ORDER, and LIMIT clauses
of a SQL statement.
# Filtering with conditions
published_posts = Post.where(status: 'published')
# Using placeholders to prevent SQL Injection
# ALWAYS use this syntax for user-provided input
secure_search = Post.where("title LIKE ?", "%#{params[:query]}%")
# Ordering results
recent_posts = Post.order(created_at: :desc)
# Limiting and Offsetting
paged_posts = Post.limit(10).offset(20)
# Selecting specific columns to save memory
post_titles = Post.select(:id, :title)
Calculations and Aggregations
Active Record provides built-in methods for performing common
mathematical operations directly in the database, which is significantly
faster than loading all records into Ruby memory to calculate them.
| Method |
Purpose |
Example |
count |
Counts the number of records. |
User.count |
average |
Calculates the average of a column. |
Product.average(:price) |
minimum / maximum |
Finds the lowest or highest value. |
Order.maximum(:total) |
sum |
Adds up all values in a column. |
Donation.sum(:amount) |
Eager Loading (Solving the N+1 Problem)
A common performance pitfall in Rails is the "N+1 query" problem. This
occurs when you load a collection of records and then perform a separate
query for each record to fetch an association. To prevent this, use
includes or joins.
# BAD: Triggers 1 query for authors + N queries for their books
authors = Author.all
authors.each { |a| puts a.books.name }
# GOOD: Triggers only 2 queries total
authors = Author.includes(:books).all
authors.each { |a| puts a.books.name }
Warning: Never use string interpolation in query methods
(e.g.,
where("name = '#{name}'")). This exposes your application
to SQL
Injection attacks. Always use the array or hash syntax provided by Rails
to safely sanitize inputs.
Scopes
Scopes allow you to define commonly-used queries inside your model,
making your controller code cleaner and more expressive.
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
scope :created_since, ->(time) { where("created_at > ?", time) }
end
# Usage:
@recent_articles = Article.published.created_since(1.day.ago)
Note
ActiveRecord::Relation objects are chainable. You
can define a scope and then chain further where,
order, or limit calls onto it
seamlessly.
Layouts and Rendering
In the Rails MVC architecture, Action View is
responsible for compiling the response sent back to the browser.
Rendering is the process of transforming your data and templates
into a final format, usually HTML. Rails distinguishes between the
Template (the specific content for an action) and
the Layout (the global wrapper containing elements
like headers, footers, and navigation). This separation ensures that
you maintain a consistent look and feel across your application
without duplicating code.
When a controller action finishes executing, Rails automatically
looks for a template file that matches the name of the controller
and the action. This is part of the "Convention over Configuration"
philosophy. However, you can explicitly control this behavior using
the render method to specify different templates,
partials, or even raw text and JSON.
The Layouts Folder
Layouts reside in the app/views/layouts directory. By
default, Rails uses application.html.erb as the global
wrapper for all controllers. Within a layout, the yield
statement is the most critical component; it identifies where the
content from the specific action template should be injected.
<%# app/views/layouts/application.html.erb %>
<!DOCTYPE html>
<html>
<head>
<title>MyDocsApp</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
</head>
<body>
<nav>
<a href="/">Home</a>
</nav>
<main>
<%# This is where the specific view content is injected %>
<%= yield %>
</main>
<footer>
<p>© 2026 Documentation Project</p>
</footer>
</body>
</html>
The render Method
The render method is versatile. While Rails defaults to
rendering the template corresponding to the current action, you can
override this logic to handle different business flows, such as
re-displaying a form when validations fail.
| Command |
Description |
Example |
render :edit |
Renders the edit.html.erb template from the current
controller. |
render :edit |
render "products/show" |
Renders a template from a different controller. |
render "admin/dashboard" |
render layout: false |
Renders the template without the global application
layout. |
render :popup, layout: false |
render json: ... |
Converts an object to JSON and sets the appropriate
HTTP headers. |
render json: @user |
render plain: ... |
Sends a plain text response to the browser. |
render plain: "OK" | Handling
Multiple Yields
Handling Multiple Yields
Sometimes a layout needs more than one insertion point—for example, a
sidebar that changes based on the page or a specific set of scripts
for the <head>. Rails provides
content_for to capture a block of code in the view and
inject it into a named yield in the layout.
<%# In the Layout: app/views/layouts/application.html.erb %>
<aside>
<%= yield :sidebar %>
</aside>
<%# In the View: app/views/articles/show.html.erb %>
<% content_for :sidebar do %>
<ul>
<li>Author: <%= @article.author %></li>
<li>Published: <%= @article.created_at.to_date %></li>
</ul>
<% end %>
<h1><%= @article.title %></h1>
<p><%= @article.content %></p>
Rendering Partial Templates
Partials are a way to break your view code into smaller, reusable
chunks. Partial filenames always begin with an underscore (e.g.,
_form.html.erb), but they are referenced without the
underscore when called.
<%# Rendering a partial for a form %>
<%= render "form", article: @article %>
<%# Rendering a collection (Automatically iterates and passes each element) %>
<%= render partial: "comment", collection: @article.comments %>
Note
When rendering a collection using
render @comments, Rails will look for a partial
named _comment.html.erb and automatically pass
a local variable named comment to each
iteration. This is a highly optimized shortcut for clean
views.
Warning: You can only call renderM or
redirect_to once per controller action. If your logic
allows for multiple paths, ensure you use return to
exit the method after the first render call, otherwise, Rails will
raise a DoubleRenderError.
| Component |
Responsibility |
| Template |
Contains the specific HTML and logic for a single
page/action. |
| Layout |
Provides the boilerplate HTML structure and global
elements. |
| Partial |
Sub-templates used for reusability (e.g., a "User
Card" or "Navigation Link"). |
| Yield |
A placeholder in the layout where the template or
content_for blocks are inserted.
|
Form Helpers
In Ruby on Rails, form helpers are designed to simplify the
creation of HTML forms while ensuring tight integration with
Active Record models. Rather than writing raw HTML
<form> and <input> tags,
developers use Ruby methods that automatically handle
authenticity tokens (for CSRF protection), generate
appropriatename and id attributes
based on model attributes, and pre-fill fields with existing
data.
Rails categorizes form helpers into two main types:
Generic Form Helpers (used for forms not tied
to a specific model) and Model-Specific Form
Helpers (used to create or update database
records).
The
form_withHelper
Starting with Rails 5.1 and standardized in later versions,
form_with is the unified method for creating forms.
It can handle both model-backed forms and basic URL-based forms.
By default, form_with submits via Ajax (XHR),
though this can be disabled by setting local: true.
| Option |
Description |
Example |
:model |
Ties the form to a specific Active Record
object. |
form_with model: @user |
:url |
Specifies the submission path manually. |
form_with url: search_path |
:method |
Defines the HTTP verb (GET, POST, PATCH,
DELETE). |
method: :patch |
:scope |
Adds a namespace to the input names. |
scope: :admin |
Model-Backed Forms
When you pass an object to form_with, Rails uses
Polymorphic Routing to determine the URL and
method. If the @article object is new (not yet
saved in the DB), the form will point to the create
action using POST. If the object already exists, it
will point to the update action using PATCH.
<%# app/views/articles/_form.html.erb %>
<%= form_with model: @article do |form| %>
<div>
<%= form.label :title, "Article Title" %>
<%= form.text_field :title, placeholder: "Enter title here..." %>
</div>
<div>
<%= form.label :content %>
<%= form.text_area :content, size: "60x10" %>
</div>
<div>
<%= form.label :status %>
<%= form.select :status, ['draft', 'published'], selected: 'draft' %>
</div>
<div>
<%= form.submit "Save Article" %>
</div>
<% end %>
Common Input Helpers
Rails provides a wide variety of helpers that map to specific
HTML5 input types, ensuring better mobile keyboard support and
data validation.
| Helper |
HTML Equivalent |
Purpose |
text_field |
<input type="text"> |
Standard single-line text input. |
password_field |
<input type="password"> |
Masks characters as they are typed. |
email_field |
<input type="email"> |
Triggers email keyboards on mobile devices. |
number_field |
<input type="number"> |
Numeric input with min, max, and step support.
|
date_select |
Multiple <select> tags |
Provides dropdowns for Year, Month, and Day.
|
check_box |
<input type="checkbox"> |
Boolean values (includes a hidden field for
"false"). |
Handling Selection and Associations
Creating dropdowns or radio buttons for associated data is a
common task. The collection_select helper is
specifically designed to generate a <select>
tag based on a collection of Active Record objects.
<%# Creating a dropdown of all Categories in the database %>
<%= form.collection_select :category_id, Category.all, :id, :name, prompt: true %>
Note
The check_box helper generates a hidden
field with a value of 0 before the actual
checkbox. This ensures that if the user unchecks the
box, a value is still sent to the controller (otherwise,
the browser would send nothing, and the attribute would
remain true in the database).
Form Security (CSRF)
Rails protects against Cross-Site Request Forgery (CSRF) by
automatically embedding an authenticity_token in
every form. When the form is submitted, Rails verifies this
token against the one stored in the user's session. If they do
not match, the request is rejected.
Warning: If you write a form using raw HTML
<form> tags instead of Rails helpers, you
must manually include the CSRF token using the
csrf_meta_tags or hidden_field_tag,
otherwise, your POST requests will fail with an
InvalidAuthenticityToken error.
Asset Pipeline (CSS & JavaScript)
The Asset Pipeline is a framework within Rails (historically
powered by Sprockets, now evolved through
Propshaft andWebpacker)
designed to process and serve frontend assets like
JavaScript, Stylesheets, and Images. Its primary purpose is
to provide a "production-ready" frontend by concatenating
files to reduce HTTP requests, minifying code to reduce file
size, and transpiling high-level languages (like Sass or
TypeScript) into browser-compatible CSS and JavaScript.
Modern Rails (Version 7+) has shifted toward import
maps as the default, which allows you to load
JavaScript modules directly in the browser without complex
bundling. However, for more intensive frontend requirements,
Rails still supports traditional bundlers like
esbuild, Rollup, or
Webpack.
The Manifest and Fingerprinting
In a production environment, Rails uses
Fingerprinting. This process appends a
unique MD5 hash based on the file content to the filename
(e.g., application-d3b073.css). This ensures
that if the content changes, the filename changes, forcing
the user's browser to bypass its cache and download the new
version.
| Asset Type |
Source Directory |
Helper Method |
| Stylesheets |
app/assets/stylesheets |
stylesheet_link_tag |
| JavaScript |
app/javascript |
javascript_importmap_tags or
javascript_include_tag
|
| Images |
app/assets/images |
image_tag |
| Fonts/Other |
app/assets/fonts |
asset_path |
Implementation: Stylesheets and Images
To include assets in your layouts, you use specific Rails
helper methods. These methods are preferred over standard
HTML tags because they automatically handle the pathing for
both development and production (fingerprinted)
environments.
<%# app/views/layouts/application.html.erb %>
<%# Link to the main stylesheet %>
<%= stylesheet_link_tag "application", media: "all", "data-turbo-track": "reload" %>
<%# Render an image from app/assets/images/logo.png %>
<%= image_tag "logo.png", alt: "Company Logo", class: "header-logo" %>
Modern JavaScript: Import Maps
With Rails 7+, the default isImport Maps.
This technology avoids the "Node.js dependency hell" by
mapping logical names to specific file paths or CDNs,
allowing the browser to manage modules natively.
# config/importmap.rb
# Pinning a local file
pin "application", preload: true
# Pinning a remote library via a CDN (JSPM)
pin "canvas-confetti", to: "https://ga.jspm.io/npm:canvas-confetti@1.6.0/dist/confetti.browser.js"
In your JavaScript file, you can then import these modules
directly:
// app/javascript/application.js
import confetti from "canvas-confetti"
document.addEventListener("turbo:load", () => {
console.log("Assets loaded via Import Maps!");
confetti();
});
Precompilation for Production
In development, Rails compiles assets on-the-fly. However, in
production, assets must be pre-generated into the
public/assets folder. This is done via a Rake
task. This step is critical because the web server (like
Nginx or Apache) will serve these static files directly for
maximum performance.
# Command to prepare assets for a production deployment
bin/rails assets:precompile
Organizing CSS with Sass
Rails includes built-in support for Sass
(Syntactically Awesome Style Sheets). This allows for
variables, nesting, and mixins, making your CSS more
maintainable. By using the .erb extension on an
asset file (e.g., custom.css.scss.erb), you can
even embed Ruby code to reference paths or configuration
variables.
/* app/assets/stylesheets/variables.scss */
$primary-color: #3b82f6;
.btn-primary {
background-color: $primary-color;
&:hover {
background-color: darken($primary-color, 10%);
}
}
Warning: Never reference assets using
hardcoded strings like/assets/logo.png. Always
use the image_tag or asset_path
helpers. If you use hardcoded strings, your images will
break in production because the fingerprinted filenames
(e.g., logo-abc123.png) will not match your
code.
Note
As of Rails 7, Turbo (part of
Hotwire) is the default. It handles page navigation
without full reloads. When writing JavaScript, use
the turbo:load event instead of
DOMContentLoaded to ensure your scripts
execute correctly when navigating between pages.
Import Maps (Modern JS Handling)
Import maps are the default mechanism for managing
JavaScript in Rails 7+. Historically, Rails relied on
heavy-duty bundlers like Webpack to transpile and bundle
JavaScript into a single file. Import maps represent a
paradigm shift by leveraging native browser support for
ES Modules (ESM). Instead of "building"
a JavaScript bundle, Rails provides the browser with a
map that translates logical module names (e.g.,
"bootstrap") into specific versioned file
paths.
This approach significantly simplifies the development
workflow by removing the requirement for Node.js or a
node_modules directory in many standard
Rails applications. It allows for faster page loads
through better caching, as changing one small JavaScript
file no longer invalidates a massive monolithic bundle.
The importmap.rb
Configuration
The heart of this system is the
config/importmap.rb file. This file acts as
a manifest where you "pin" your dependencies. When you
pin a library, you are telling Rails where to find the
source code, whether it is a local file within your
application or a remote package hosted on a CDN
(Content Delivery Network) like JSPM or
UNPKG.
# config/importmap.rb
# Pin the main entry point of the application
pin "application", preload: true
# Pin a local component
pin "dropdown_controller", to: "controllers/dropdown_controller.js"
# Pin a remote library from a CDN
pin "canvas-confetti", to: "https://ga.jspm.io/npm:canvas-confetti@1.6.0/dist/confetti.browser.js"
Usage in JavaScript Files
Once a library is pinned, you can use the standard
JavaScript import syntax. The browser
intercepts these requests, looks at the map provided in
the HTML header, and fetches the correct file.
// app/javascript/application.js
import { confetti } from "canvas-confetti"
document.addEventListener("turbo:load", () => {
console.log("Import maps are handling this module!");
confetti();
});
Managing Dependencies via CLI
Rails provides a dedicated command-line tool to manage
your import map pins. This tool automatically fetches
the correct URLs from CDNs and updates
yourconfig/importmap.rb file, ensuring that
sub-dependencies (transitively required libraries) are
also pinned.
| Command |
Action |
bin/importmap pin jquery
|
Finds and pins the jQuery library to the
config file. |
bin/importmap unpin jquery
|
Removes the library from the config
file. |
bin/importmap json |
Outputs the raw JSON map that will be
sent to the browser. |
bin/importmap audit |
Checks for known security
vulnerabilities in pinned packages. |
Preloading for Performance
One potential downside of ES Modules is "waterfalling,"
where the browser discovers it needs a dependency only
after downloading the parent file. Rails mitigates this
using the preload: true option in the pin.
This adds a
<link rel="modulepreload"> tag to the
HTML head, instructing the browser to begin downloading
critical scripts immediately.
<%# app/views/layouts/application.html.erb %>
<head>
<%# This helper generates the actual <script type="importmap"> block and preloads %>
<%= javascript_importmap_tags %>
</head>
Note
Import maps work best with libraries that are
distributed as ES Modules. If you need to use
older "legacy" JavaScript libraries that rely on
global variables or CommonJS
(require), you may still need to
use a bundler like esbuild.
Warning: Since Import Maps rely on
native browser features, they are supported only in
modern browsers. While this covers >95% of users, if
your application must support very old browsers (like
Internet Explorer), you will need to use a transpiler
and bundler instead of Import Maps.
| Feature |
Import Maps |
Traditional Bundlers
(Webpacker/esbuild) |
| Node.js Required |
No |
Yes |
| Build Step |
None (Native ESM) |
Required (Transpilation/Minification)
|
| Caching |
Granular (Per-file) |
Coarse (The whole bundle) |
| Complexity |
Low |
High |
Controller Overview
In the Rails MVC architecture, the
Controller acts as the central
coordinator or the "brain" of the request-response
cycle. Its primary responsibility is to receive an
incoming HTTP request from the router, interact with
the Model to retrieve or persist
data, and then instruct the View to
render the appropriate output. In a well-architected
Rails application, controllers should remain
"skinny," meaning they contain only the logic
necessary to direct data between the Model and the
View, while complex business logic is encapsulated
within the Model or Service Objects.
Every controller in a Rails application is a Ruby
class that inherits from
ApplicationController, which in turn
inherits from ActionController::Base.
This inheritance chain provides the controller with
a vast array of methods for handling sessions,
cookies, redirects, and rendering.
Naming and Directory Structure
Following the Convention over
Configuration principle, controllers
are named using the plural form of the resource they
manage, followed by the suffix
Controller. The file names must be
snake_case, while the class names are CamelCase.
| Resource |
Controller Class Name
|
File Path |
| Article |
ArticlesController |
app/controllers/articles_controller.rb
|
| User |
UsersController |
app/controllers/users_controller.rb
|
| Admin Setting |
Admin::SettingsController
|
app/controllers/admin/settings_controller.rb
|
The Action Lifecycle
A controller is composed of public methods
called Actions. When a request
matches a route, Rails instantiates the controller
and calls the specific action method. Once the
method completes, Rails automatically looks for a
view template that matches the action name unless an
explicit render
orredirect_to is specified.
class ArticlesController < ApplicationController
# GET /articles
def index
# Interacting with the Model
@articles = Article.all
# Implicitly renders app/views/articles/index.html.erb
end
# GET /articles/:id
def show
@article = Article.find(params[:id])
end
end
Theparams
Hash
All data sent with a request—whether it comes from a
URL segment (like /articles/1), a query
string (like ?page=2), or a submitted
form—is accessible through the params
hash. This object behaves like a Ruby hash but
allows you to access keys using either strings or
symbols interchangeably.
| Param Type |
Source Example |
Access Method |
| Path Parameter |
/photos/:id |
params[:id] |
| Query String |
/search?q=ruby |
params[:q] |
| Form Data |
<input name="user[name]">
|
params[:user][:name]
|
Strong Parameters
As a security measure to prevent Mass
Assignment vulnerabilities (where a
malicious user could pass extra fields like
admin=true into a form), Rails requires
developers to explicitly whitelist which parameters
are allowed for database operations. This is handled
via the require and permit
methods.
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render :new
end
end
private
# Best Practice: Encapsulate logic in a private method
def user_params
params.require(:user).permit(:username, :email, :password)
end
end
Warning: Failure to use Strong
Parameters will result in an
ActiveModel::ForbiddenAttributesError
when trying to pass the entire params
hash directly into a model's .new or
.create methods.
Note
The ApplicationController as a
"base" for your specific controllers. You
can define methods or
before_action filters here that
should apply to every controller in your
application, such as user authentication or
setting the user's locale.
Routing (The routes.rb file)
The Rails Router is the engine that connects
incoming HTTP requests to the appropriate
controller actions. When your Rails application
receives a request, it consults the
config/routes.rb file to find a
match. This file acts as a centralized map,
defining how URLs should be interpreted. Routing
in Rails is designed to be
RESTful, meaning it maps HTTP verbs
(GET, POST, PATCH, DELETE) and URLs to standard
CRUD (Create, Read, Update, Delete) operations.
In addition to mapping requests, the router also
generates "URL helpers." These are Ruby methods
that allow you to reference paths dynamically in
your views and controllers, ensuring that if you
change a URL structure in the routes file, you
don't have to manually update every link in your
application.
Resourceful Routing
The most common way to define routes is via
theresources keyword. A single line
of code can generate seven different routes for
a model, following the REST convention. This
keeps the routes file clean and ensures the
application follows predictable web standards.
# config/routes.rb
Rails.application.routes.draw do
# Generates 7 standard routes for the Articles resource
resources :articles
end
The resources :articles declaration
creates the following mapping:
| HTTP Verb |
Path |
Controller#Action
|
Named Helper |
| GET |
/articles |
articles#index |
articles_path |
| GET |
/articles/new |
articles#new |
new_article_path
|
| POST |
/articles |
articles#create
|
articles_path |
| GET |
/articles/:id |
articles#show |
article_path(:id)
|
| GET |
/articles/:id/edit
|
articles#edit |
edit_article_path(:id)
|
| PATCH/PUT |
/articles/:id |
articles#update
|
article_path(:id)
|
| DELETE |
/articles/:id |
articles#destroy
|
article_path(:id)
|
Customizing Resources
Sometimes you don't need all seven default
routes, or you need to add specific
functionality that doesn't fit the standard CRUD
pattern. Rails allows you to restrict routes
using only or except,
and add custom actions using member
or collection blocks.
resources :photos, only: [:index, :show] do
# Member routes act on a specific record (requires an ID)
member do
get 'preview' # /photos/:id/preview
end
# Collection routes act on the whole set
collection do
get 'search' # /photos/search
end
end
The Root Route
The root route is a special designation that
determines what a user sees when they visit the
base URL of your application
(e.g., www.example.com). It should
always be defined at the top or bottom of the
draw block.
# Maps the home page to the index action of the HomeController
root "home#index"
Non-Resourceful (Direct) Routes
For pages that don't represent a database
resource (like an "About" or "Contact" page),
you can define manual GET routes.
get "/about", to: "pages#about", as: :about_us
to: specifies the
controller#action.
as: creates the named helper
methods (about_us_path and
about_us_url).
Nested Resources
When one resource logically belongs to another
(e.g., Comments belonging to an Article), you
can nest them to reflect this relationship in
the URL structure.
resources :articles do
resources :comments
end
# Resulting path: /articles/:article_id/comments/:id
Warning: Avoid nesting resources
more than one level deep. Deeply nested routes
(e.g.,
/users/1/articles/5/comments/10)
result in long, confusing URLs and make
controller logic significantly more complex. Use
"Shallow Nesting" if you need to reference a
child's ID directly.
Note
You can view all defined routes in your
application at any time by running
bin/rails routes in your
terminal or by
visiting /rails/info/routes
your browser while in development mode.
Parameters & Strong Parameters
In Rails, Parameters are the
data sent by the user to the application via
the browser. This data can arrive through
the URL path, a query string, or a submitted
form. All these inputs are collected into a
single, convenient object called
params. Because this data comes
from an external source (the user), it is
inherently untrusted. Strong
Parameters is the security
layer that prevents "Mass Assignment"
vulnerabilities by forcing developers to
explicitly whitelist which attributes are
allowed to be passed to the database.
The params
Hash
The params object is an instance
of
ActionController::Parameters.
While it acts like a Ruby hash, it provides
security methods and allows you to access
keys using either strings or symbols.
| Parameter Type
|
Example
URL/Source |
Access Syntax
|
| Path Params
|
/users/:id
(e.g.,
/users/5)
|
params[:id] #
=> "5" |
| Query
Params |
/search?q=ruby&page=1
|
params[:q] # =>
"ruby" |
| Body Params
|
Form inputs like
name="user[email]"
|
params[:user][:email]
|
Mass Assignment Security
Mass Assignment is a feature where you pass a
hash of attributes to a model to create or
update a record in one go
(e.g.,User.new(params[:user])).
Without Strong Parameters, a malicious user
could add an unauthorized field to the
request, such asadmin: true,
and potentially grant themselves
administrative privileges.
Strong Parameters solve this by requiring two
specific actions:
- Require: Ensures that a
specific key (usually the model name) is
present in the
params hash.
- Permit: Specifies which
sub-attributes are allowed to be
processed.
Standard Implementation
The best practice is to wrap parameter logic
in a private method at the
bottom of your controller. This keeps your
actions clean and allows the logic to be
reused across create and
update actions.
class ProductsController < ApplicationController
def create
# Only the permitted attributes will be passed to .new
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new
end
end
private
def product_params
# 1. Look for the :product key
# 2. Only allow :name, :description, and :price
params.require(:product).permit(:name, :description, :price)
end
end
Handling Complex Data
Sometimes parameters are not just simple
strings. You may need to handle arrays (like
tags) or nested data (like an address
belonging to a user).
| Scenario |
Code Example
|
| Scalar
Values |
params.permit(:title, :content)
|
| Arrays |
params.permit(tags: [])
|
| Nested
Hashes |
params.require(:user).permit(:name, address: [:street, :city])
|
Handling "Unpermitted
Parameters"
By default, Rails will simply ignore any keys
that are not explicitly permitted. However,
in a development environment, Rails will log
a message in your terminal:
Unpermitted parameter: :hacker_field.
- To raise an error in
development: You can
configure Rails to raise an exception if
unpermitted keys are found by setting
config.action_controller.action_on_unpermitted_parameters = :raise
in your
config/environments/development.rb.
Warning: Never use
params.permit!. This method
permits every key in the
hash, effectively disabling the security
benefits of Strong Parameters and leaving
your database vulnerable to mass assignment
attacks.
Note
If you are building an API that
receives JSON, Rails automatically
parses the JSON body
into the params hash, allowing you
to use the same require
and permit logic as you
would with a standard HTML form.
Session & Cookies
In a standard web environment, HTTP is
stateless, meaning the
server treats every request as a
completely new interaction with no
memory of previous ones. To build
features like user logins or shopping
carts, Rails uses
Sessions and
Cookies to maintain
state between requests.
A CookieM is a small
piece of data stored on the user's
browser. A Session is a
higher-level abstraction that allows you
to store data about a user that persists
as they move from page to page.
Sessions vs. Cookies
While they work together, they serve
different roles in data management and
security.
| Feature |
Cookies |
Sessions
|
| Storage
Location |
Client's Browser |
Server-side (managed by
Rails) |
| Data
Capacity |
Limited (~4KB) |
Virtually unlimited
(depending on store)
|
| Security
|
Visible/Editable by user
(unless
signed/encrypted) |
Highly secure; user only
sees a "Session ID" |
| Common Use
|
Remember me, Tracking,
Preferences |
Login status, Shopping
carts, User IDs |
Working with the Session
The session method in Rails
works like a Ruby hash. You can store
values in it during one action and
retrieve them in another. Rails
automatically encrypts the session
cookie to prevent users from tampering
with the data.
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
# Storing the user_id in the session
session[:user_id] = user.id
redirect_to root_path, notice: "Logged in!"
end
end
def destroy
# Removing data from the session
session[:user_id] = nil
redirect_to root_path, notice: "Logged out!"
end
end
Working with Cookies
You can also interact with cookies
directly. This is useful for data that
doesn't need to be strictly tied to a
"session" (like a "dark mode"
preference) or for "Remember Me"
functionality.
- Plain Cookies:
Visible to the user in the browser
console.
- Signed Cookies:
Cryptographically signed so the user
cannot change them.
- Encrypted Cookies:
Signed and encrypted so the user
cannot even read the content.
# Setting a permanent, encrypted cookie
cookies.encrypted.permanent[:login_token] = "secret_token_123"
# Reading a cookie
token = cookies.encrypted[:login_token]
# Deleting a cookie
cookies.delete(:login_token)
Session Storage Options
By default, Rails uses the
CookieStore. This means the
entire session hash is encrypted and
stored in a cookie on the user's
browser. For larger applications, you
might change this
in config/initializers/session_store.rb.
| Store Type
|
Pros |
Cons |
| CookieStore
|
Default, zero-config,
fast. |
4KB limit; can't easily
"invalidate" all
sessions. |
| CacheStore
|
Extremely fast (uses
Memcached/Redis). |
Data is volatile; if the
cache clears, users are
logged out. |
|
ActiveRecordStore |
Permanent; allows
managing active
sessions. |
Slower (requires
database hits). |
The
flashHash
The flash is a special part
of the session that only persists until
the next request. It is
designed specifically for passing
success or error messages to the user.
def update
if @article.update(article_params)
flash[:success] = "Article updated!"
redirect_to @article
else
# use flash.now when rendering (current request)
flash.now[:error] = "Update failed."
render :edit
end
end
Warning: Never store
large objects (like a full
User model) directly in the
session. This will exceed the 4KB limit
and degrade performance. Instead, store
only the ID of the
object (e.g.,
session[:user_id] = user.id)
and look up the record in the
controller.
Hash
Request Forgery Protection
Cross-Site Request Forgery (CSRF) is
an attack where a malicious site
tricks a user’s browser into
performing an unwanted action on a
different site where the user is
currently authenticated. Because
browsers automatically include
cookies (and thus session data) with
every request to a specific domain,
a blind request from a malicious
site could delete data or change
passwords without the user's
knowledge.
Rails provides built-in protection
against this by ensuring that every
"non-GET" request (POST, PUT, PATCH,
DELETE) originates from your own
application.
How It Works: The Authenticity
Token
When CSRF protection is enabled,
Rails generates a unique,
non-guessableauthenticity
token for each user
session.
- The Server:
Embeds this token in every form
(as a hidden field) and in the
HTML head (as a meta tag).
- The Client:
When a form is submitted, the
token is sent back to the
server.
- The
Verification: Rails
compares the token in the
request with the token stored in
the user's session. If they do
not match, Rails resets the
session or raises an error.
Implementation in
Controllers
By default, the
ApplicationController
includes a call to
protect_from_forgery.
This applies the protection to every
controller in your app that inherits
from it.
class ApplicationController < ActionController::Base
# Enables CSRF protection
protect_from_forgery with: :exception
end
| Strategy
|
Behavior
on Mismatch |
Use Case
|
:exception
|
Raises
ActionController::InvalidAuthenticityToken.
|
Standard web
applications (strict
security). |
:null_session
|
Clears the session
for the duration of
the request. |
APIs that support
cookie-based auth
but allow external
calls. |
:reset_session
|
Completely destroys
the user's session.
|
High-security
applications to
prevent further
hijacking. |
Ensuring Tokens are
Included
If you use Rails helpers, the token
is managed for you automatically.
- Forms:
form_with or
form_tag
automatically inserts a hidden
<input type="hidden" name="authenticity_token">.
- AJAX/JavaScript:
If you are using Fetch or XHR,
you must pull the token from the
meta tags and include it in the
X-CSRF-Token
header.
<%# app/views/layouts/application.html.erb %>
<html>
<head>
<%# Generates: <meta name="csrf-token" content="..."> %>
<%= csrf_meta_tags %>
</head>
...
</html>
Skipping Protection
In specific cases, such as an
API that uses
Token-based authentication (like
JWT) instead of Cookies, you may
need to disable CSRF protection
because the request does not rely on
browser cookies to authenticate.
class Api::V1::BaseController < ActionController::Base
# Skip CSRF protection for API calls
skip_before_action :verify_authenticity_token
end
Best Practices
| Rule
|
Reason
|
| Use GET only
for
reading
|
Browsers and Rails
assume GET requests
are "safe" and do
not check for
tokens. Never change
data in a GET
action. |
| Use Rails
Form
Helpers
|
Prevents forgetting
the hidden token
field. |
Keep
csrf_meta_tags
|
Essential for
JavaScript-based
requests (like Turbo
or Axios) to
function. |
Warning: If you
receive an
InvalidAuthenticityToken
error in development, it usually
means your session has expired,
you've hardcoded an HTML form
without a token, or you are trying
to submit a form via JavaScript
without including the CSRF header.
Active Job (Background Tasks)
Active Job is a framework for declaring
background jobs and making them run
on a variety of queuing backends. These jobs can be anything
from regularly scheduled
cleanups to billing charges or sending emails. The primary
goal is to ensure that "heavy"
tasks are handled outside of the main request-response
cycle, keeping your application
snappy and responsive for the user.
Without background jobs, a user would have to wait for a slow
process (like generating a PDF
or hitting a third-party API) to finish before the page
reloads. With Active Job, you
"enqueue" the task and immediately tell the user, "We're
working on it!"
Why Use Background Jobs?
The main request-response loop should ideally take less than
200ms. If a
task takes longer, it should be offloaded.
| Use Case |
Description |
| Email Delivery |
Sending newsletters or password resets
without pausing the UI. |
| Image Processing |
Resizing or watermarking uploaded photos.
|
| Third-Party APIs |
Syncing data with Salesforce, Stripe, or
Slack. |
| Heavy Calculations |
Generating complex financial reports or data
exports. |
Creating and Enqueuing a Job
You can generate a new job using the Rails
generator: bin/rails generate job ProcessReport.
This creates a class in
app/jobs.
# app/jobs/process_report_job.rb
class ProcessReportJob < ApplicationJob
queue_as :default
# The logic to be executed in the background
def perform(user_id)
user = User.find(user_id)
# Simulate heavy work
ReportGenerator.generate_for(user)
end
end
To trigger this job from a controller or model, you use
perform_later:
# Enqueue for as soon as the queue is free
ProcessReportJob.perform_later(current_user.id)
# Enqueue to run at a specific time
ProcessReportJob.set(wait: 1.hour).perform_later(current_user.id)
Supported Queuing Backends
Active Job is an adapter layer. In development, it uses the
:async adapter
(in-memory), but for production, you need a persistent
store.
| Backend |
Storage |
Best For |
| Sidekiq |
Redis |
High-performance, multi-threaded execution
(Most Popular). |
| Solid Queue |
Database (SQL) |
Simplicity; no need for a separate Redis
server (Rails 8 default). |
| Delayed Job |
Database (SQL) |
Ease of setup in environments where Redis
isn't available. |
| Amazon SQS |
AWS Cloud |
Large-scale enterprise applications on AWS.
|
GlobalID: Passing Objects to Jobs
Active Job uses GlobalID to handle
arguments. Instead of passing a large
Ruby object (which would fail to serialize into the queue),
you pass the object itself, and
Rails automatically converts it to a URI.
# Rails handles this automatically
# Enqueue: "gid://app/User/1"
ProcessReportJob.perform_later(@user)
# Perform: Rails finds the user by ID before running the method
def perform(user)
puts user.email
end
Callbacks and Retries
Jobs can fail (e.g., an API is down). Active Job provides
built-in mechanisms to handle these
failures gracefully.
class ProcessReportJob < ApplicationJob
# Automatically retry the job if the remote server is down
retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 5
# Log a specific message if the record was deleted before the job ran
discard_on ActiveRecord::RecordNotFound
before_perform do |job|
# Logic to run before the job starts
end
end
Warning: Never pass sensitive data or large
files directly as arguments to a
job. Pass the database ID or a file
path. The queue store
(like Redis) is often unencrypted and has size limits.
Note
Remember that background jobs run in a separate
process. They do not have access to
the session, cookies, or
current_user from
your controller. You must pass any necessary data as
arguments.
Action Mailer (Sending Emails)
Action Mailer allows you to send emails from
your application using a
philosophy similar to controllers and views. Mailers inherit
from
ActionMailer::Base and reside in
app/mailers. Just as a controller
action renders a view to produce an HTML response for a
browser, a mailer action renders a
template to produce the body of an email.
Mailer Components
A mailer setup consists of three primary parts:
- The Mailer Class: Defines the "action"
and sets the
to,
from, and subject headers.
- The Mailer View: An .
erb
template (HTML or Plain Text)
that defines the email content.
- The Configuration: Settings in
config/environments that
define the delivery method (SMTP, SendGrid, Postmark,
etc.).
Creating a Mailer
Use the generator to create a new mailer:
bin/rails generate mailer UserMailer.
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
default from: 'notifications@example.com'
def welcome_email(user)
@user = user
@url = 'http://example.com/login'
# mail() triggers the rendering of the template
mail(to: @user.email, subject: 'Welcome to My Awesome Site')
end
end
Mailer Views
Emails should generally provide both HTML and Plain Text
versions to ensure compatibility
across all mail clients. Rails looks for both files
automatically if they share the same
name.
- HTML:
app/views/user_mailer/welcome_email.html.erb
- Text:
app/views/user_mailer/welcome_email.text.erb
<%# app/views/user_mailer/welcome_email.html.erb %>
<h1>Welcome to Example.com, <%= @user.name %></h1>
<p>
You have successfully signed up. Your username is: <%= @user.username %>.<br>
</p>
<p>
To login to the site, just follow this link: <%= link_to 'Login', @url %>.
</p>
Sending Emails
Mailers integrate seamlessly with Active
Job. You should almost always use
deliver_later to avoid making the user wait for
the SMTP server to respond
during a web request.
| Method |
Execution Type |
Description |
deliver_now |
Synchronous |
Sends the email immediately. Blocks the
current process. |
deliver_later |
Asynchronous |
Enqueues the email as a background job
(Recommended). |
# From a controller action
UserMailer.welcome_email(@user).deliver_later
Previewing Emails
Rails provides a built-in way to preview emails in the
browser without actually sending them.
This is located in test/mailers/previews. You
can view these at
localhost:3000/rails/mailers.
# test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
def welcome_email
UserMailer.welcome_email(User.first)
end
end
Delivery Methods & Configuration
In your environment files (e.g.,
config/environments/production.rb), you must
specify how Rails should hand off the email.
| Method |
Best For |
Description |
:test |
Testing |
Emails are added to an array
(ActionMailer::Base.deliveries)
rather than sent. |
:letter_opener |
Development |
Opens the email in a new browser tab instead
of sending it. |
:smtp |
Production |
Connects to a standard mail server (Gmail,
Outlook, etc.). |
:sendmail |
Unix Systems |
Uses the system's local sendmail
installation. |
Warning: Emails sent from Rails use
Absolute URLs. In your
mailer views, you must use _url helpers (e.g.,
user_url(@user))
instead of_path helpers, and you must configure
config.action_mailer.default_url_options so
Rails knows your domain name.
Action Text (Rich Text Content)
Action Text makes it easy to handle and
display rich text content (formatted
text, images, and attachments) in Rails. It integrates the
Trix editor into
the framework, handling everything from the frontend user
interface to the backend storage
of attachments via Active Storage.
Instead of storing raw HTML in a standard string or text
column, Action Text stores the
content in a separate ActionText::RichText
model that is associated with your
existing models. This keeps your main database tables lean
and provides a standardized way
to handle complex content.
Key Features
- WYSIWYG Editing: Provides the Trix
editor out of the box for bolding,
lists, and links.
- Integrated Attachments: Users can drag
and drop images or files
directly into the editor.
- Seamless Transformation: Automatically
handles the conversion of
embedded attachments into optimized HTML tags.
Setup and Installation
To use Action Text, you must first run the installer to
create the necessary database tables
and migration files.
# Install Action Text and Active Storage dependencies
bin/rails action_text:install
bin/rails db:migrate
Implementation in Models and Views
You don't need to add a column to your model's table.
Instead, you declare the rich text
attribute in the model file using
has_rich_text.
1. The Model
# app/models/article.rb
class Article < ApplicationRecord
# This creates a virtual attribute 'content'
has_rich_text :content
end
2. The Form View Use the
rich_text_area helper to render the
Trix editor in your forms.
<%# app/views/articles/_form.html.erb %>
<%= form_with model: @article do |form| %>
<%= form.label :content %>
<%= form.rich_text_area :content %>
<%= form.submit %>
<% end %>
3. Displaying Content display the formatted
content, simply render the
attribute. Rails will handle the sanitization and attachment
rendering.
<%# app/views/articles/show.html.erb %>
<h1><%= @article.title %></h1>
<div>
<%= @article.content %>
</div>
Comparison: Plain Text vs. Action Text
| Feature |
Standard text Column |
Action Text |
| Formatting |
Plain text only (unless manual HTML) |
Rich text (Bold, Italics, Lists) |
| Images |
Requires manual URL handling |
Drag-and-drop via Active Storage |
| Database |
Stored in the model's table |
Stored in
action_text_rich_texts
|
| Sanitization |
Manual or none |
Automatic via Rails |
Handling Attachments
When a user uploads an image through Action Text, it is
stored using Active
Storage. You can customize how these
attachments are rendered by modifying the
partial located at
app/views/active_storage/blobs/_blob.html.erb.
Note
Because Action Text content is stored in a separate
table, you should use eager
loading to avoid N+1 queries when displaying a list
of records:
Article.all.with_rich_text_content
Warning: Action Text content is
sanitized to prevent XSS attacks. If
you need to allow specific HTML tags that are
stripped by default, you must
configure the
ActionText::ContentHelper.allowed_tags
in an initializer.
Active Storage (File Uploads)
Active Storage is a built-in Rails framework
that facilitates uploading
files to a cloud storage service (like Amazon S3, Google
Cloud Storage, or Microsoft Azure
Storage) or a local disk for development. It attaches those
files to Active Record objects
without requiring you to add specific columns to your
model's table for file paths.
Instead of managing file metadata manually, Active Storage
uses two primary database tables:
active_storage_blobs (metadata like filename
and content type) and
active_storage_attachments (the "join" table
connecting the blob to your
model).
Key Features
- Polymorphic Links: Attach files to any
model using a simple
declaration.
- Image Processing: Built-in support for
generating variants (resizing,
cropping) using Vips or ImageMagick.
- Direct Uploads: Allows the browser to
upload files directly to the
cloud, bypassing your Rails server to save bandwidth and
memory.
- Previewing: Generates image previews
for non-image files like PDFs and
videos.
Setup and Configuration
First, run the installer to generate the necessary migrations
for the metadata tables:
bin/rails active_storage:install
bin/rails db:migrate
Next, configure your storage services in
config/storage.yml and point to the
desired service in your environment files (e.g.,
config/environments/development.rb
).
# config/storage.yml
local:
service: Disk
root: <%= Rails.root.join("storage") %>
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
# ... other AWS config
Attaching Files to Models
You can define either a single attachment or multiple
attachments on a model.
| Macro |
Usage |
Example |
has_one_attached |
For a single file (e.g., Avatar). |
has_one_attached :avatar |
has_many_attached |
For multiple files (e.g., Gallery). |
has_many_attached :photos |
# app/models/user.rb
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
# Define variants directly in the model (Rails 7+)
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
Working with Attachments in Views
Rails provides helpers to generate file inputs and display
the attached files.
1. The Form
<%= form_with model: @user do |form| %>
<%= form.file_field :avatar %>
<%= form.submit %>
<% end %>
2. Displaying the Image
<% if @user.avatar.attached? %>
<%# Display the processed thumbnail variant %>
<%= image_tag @user.avatar.variant(:thumb) %>
<% end %>
Comparison: Attachment Types
| Feature |
has_one_attached |
has_many_attached |
| Logic |
Replaces any existing file on upload. |
Appends new files to the collection. |
| Access |
Returns a single object. |
Returns an
ActiveStorage::Attached::Many
collection.
|
| Use Case |
Profile pictures, resumes, single logos.
|
Image galleries, document attachments. |
Performance: Eager Loading
Because Active Storage uses separate tables, displaying a
list of records with their
attachments can cause N+1 queries. Use the provided scopes
to fetch everything in one go:
# Fetches users and their avatars in a single query
@users = User.with_attached_avatar.all
Warning: To use image resizing/variants, you
must have the
libvips or ImageMagick library
installed on your system and the
image_processing gem in your Gemfile.
Note
For security, Active Storage serves files via a
redirection controller. When you call
url_for(user.avatar), Rails generates a
short-lived URL that expires,
preventing unauthorized hotlinking of your private
files.
Action Cable (WebSockets/Real-time)
Action Cable integrates
WebSockets into the Rails
framework, allowing for real-time, bi-directional
communication between the server and the
client. Unlike traditional HTTP requests where the client
must poll the server for updates,
Action Cable maintains a persistent connection. This enables
features like live chat,
instant notifications, and real-time dashboard updates.
It combines a JavaScript framework on the client side with a
Ruby framework on the server
side, providing a "full-stack" approach to real-time
features.
Core Concepts
Action Cable uses a "Pub/Sub" (Publisher/Subscriber) model to
manage data flow.
| Concept |
Responsibility |
| Connection |
A single WebSocket connection per browser
tab/window. Handled by
ApplicationCable::Connection.
|
| Channel |
Similar to a Controller, it encapsulates the
logic for a specific stream
(e.g., ChatChannel). |
| Pub/Sub |
The mechanism (usually backed by Redis) that
broadcasts messages to all
subscribers. |
| Broadcasting |
The act of sending data from the server to
one or more channels. |
Implementation: Server Side
To create a real-time feature, you first define a Channel.
You can generate one using
bin/rails generate channel Chat.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
# Called when the consumer has successfully subscribed to the channel
def subscribed
stream_from "chat_room"
end
# Called when data is sent from the client-side channel
def speak(data)
# Broadcast the received message to everyone subscribed to "chat_room"
ActionCable.server.broadcast("chat_room", message: data['message'])
end
end
Implementation: Client Side
On the frontend, your JavaScript "subscribes" to the channel
and defines how to handle
incoming data.
// app/javascript/channels/chat_channel.js
import consumer from "channels/consumer"
consumer.subscriptions.create("ChatChannel", {
connected() {
console.log("Connected to the chat room!");
},
received(data) {
// This code runs whenever the server broadcasts to "chat_room"
const chatBox = document.getElementById('chat-box');
chatBox.innerHTML += `<p>${data.message}</p>`;
},
send_message(text) {
// Calls the 'speak' method on the server-side ChatChannel
this.perform('speak', { message: text });
}
});
Broadcasting from Anywhere
One of the most powerful features of Action Cable is the
ability to trigger a broadcast from
other parts of your application, such as a Controller or a
Background Job.
# From a controller after a record is saved
def create
@message = Message.create(message_params)
if @message.save
ActionCable.server.broadcast("chat_room", message: @message.content)
end
end
Deployment & Dependencies
Action Cable requires a "Subscription Adapter" to manage the
delivery of messages.
- Development: Uses the
async adapter (in-memory, works
within a single process).
- Production: Requires
Redis. Since production Rails
apps usually run multiple processes (Puma workers),
Redis acts as the central hub that
coordinates broadcasts across all processes.
Note
In Rails 7+, Action Cable is often used in tandem
with Hotwire (Turbo
Streams). Turbo Streams uses Action
Cable behind the scenes to update
parts of a page automatically without writing custom
JavaScript.
Warning: Be mindful of
authentication. Because WebSockets are
long-lived, you should authenticate the connection
once in
app/channels/application_cable/connection.rb
(usually by checking the
session a signed cookie)
to prevent unauthorized users
from subscribing to private data streams.
Securing Rails Applications
Rails is famous for its "Secure by Default" philosophy. It
includes built-in mitigations for
the most common web vulnerabilities identified by the
OWASP Top 10.
However, security is a
process, not a checkbox. While the framework provides the
tools, developers must implement
them correctly to protect user data and maintain application
integrity.
Built-in Protections
Rails provides automatic defense against several major attack
vectors without requiring
manual configuration.
| Attack Type |
Rails Defense Mechanism |
Description |
| Cross-Site Scripting (XSS) |
Automatic Escaping |
All strings rendered in ERB templates are
escaped by default.
<script> becomes
<script>.
|
| Cross-Site Request Forgery (CSRF) |
Authenticity Tokens |
Requires a unique token for all non-GET
requests to ensure they originate
from your app. |
| SQL Injection |
Parameterized Queries |
Active Record automatically escapes input in
find(id) or
where(name: params[:name]).
|
| Clickjacking |
X-Frame-Options |
Default headers prevent your site from being
embedded in an
<iframe> on malicious
domains.
|
Password Security (Has_secure_password)
Never store passwords in plain text. Rails provides the
has_secure_password
macro, which uses the BCrypt hashing
algorithm to salt and hash passwords
before they hit the database.
# app/models/user.rb
class User < ApplicationRecord
# Requires a 'password_digest' column in the database
has_secure_password
validates :password, length: { minimum: 8 }
end
Secure Header Management
Rails uses the ActionDispatch::SSL middleware
and the
content_security_policy initializer to manage
browser-level security headers.
- Force SSL: In
config/environments/production.rb, setting
config.force_ssl = true ensures all traffic
is encrypted and cookies are
marked as Secure.
- Content Security Policy (CSP): Defines
which dynamic resources (JS,
CSS, Images) are allowed to load, significantly reducing
the impact of XSS attacks.
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self, :https
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.object_src :none
policy.script_src :self, :https
end
The Rails Security Audit Tools
Maintaining a secure application requires regular scanning of
dependencies for known
vulnerabilities.
| Tool |
Purpose |
Command |
| Bundler-Audit |
Scans Gemfile.lock for
vulnerable gem versions. |
bundle exec bundle-audit check --update
|
| Brakeman |
A static analysis security scanner for Rails
code. |
bundle exec brakeman |
| Rails Audit |
Built-in task for basic security checks
(Rails 7+). |
bin/rails app:update (checks
config) |
Best Practices for Developers
- Avoid
html_safe and
raw: Only use these when
you are 100% certain the content is not user-generated.
- Use Strong Parameters: Never pass
params directly to a
model; always whitelist attributes.
- Secret Key Base: Ensure your
master.key or
credentials.yml.enc is never committed to
version control. This key is used
to sign/encrypt cookies and sessions.
- Rate Limiting: Use gems like
rack-attack to prevent
brute-force login attempts and DDoS attacks.
Warning: While Active Record protects
against SQL injection in standard
hash-based queries, it does not protect you
if you pass a raw string with
interpolation to a where clause.
\
- Bad:
User.where("name = '#{params[:name]}'")
- Good:
User.where("name = ?", params[:name])
Debugging Rails Applications
Debugging is an essential skill in Rails development, moving
from simple log inspection to
interactive code execution. Rails provides a suite of tools
to help you track down bugs in
the logic, the database, or the view layer.
1. The Rails Logger
The logger is your first line of defense. Every request,
database query, and background job
is logged to log/development.log. You can
manually inject messages into this
log to track variable states.
| Log Level |
Usage |
Example |
| debug |
Detailed info for troubleshooting. |
logger.debug "User: #{@user.inspect}"
|
| info |
General confirmation of app flow. |
logger.info "Payment processed"
|
| warn |
Unexpected but non-fatal events. |
logger.warn "Retry attempt 2"
|
| error |
Fatal errors that stop a process. |
logger.error "DB Connection failed"
|
2. Interactive Debugging
with debug
Since Rails 7, the debug gem is the default tool
for pausing code execution. By
placing the binding.break (or the older
debugger) command in your
code, Rails will freeze the execution and open an
interactive terminal where the code
stopped.
def update
@article = Article.find(params[:id])
binding.break # The app pauses here
if @article.update(article_params)
redirect_to @article
else
render :edit
end
end
Common Debugger Commands:
c (next): Move to the next line.
s (step): Step into a method call.
c (continue): Resume normal execution.
info: View local variables.
3. The Rails Console
The console (bin/rails c) is a "Read-Eval-Print
Loop" (REPL) that loads your
entire Rails environment. It is the best place to test Model
logic or verify data without
refreshing the browser.
- Sandbox Mode:
Run
bin/rails c --sandbox to test commands
that modify the database. All changes will be rolled
back when you exit the console.
- Reloading: If you change your code
while the console is open, run
reload! to pull in the latest changes.
4. Web-Based Debugging
Rails includes several built-in views to assist with
debugging in the development
environment.
| Tool |
Access |
Description |
| Web Console |
Bottom of error pages |
An interactive terminal inside the browser
at the point of an exception.
|
| Error Page |
Browser on crash |
Shows the stack trace, extracted source
code, and request parameters. |
| Rails Routes |
/rails/info/routes |
A searchable list of all URL patterns and
their controllers. |
5. Debugging in Views
Sometimes you need to see what an object looks like directly
on the webpage.
debug(object): Renders the object in a YAML
format inside
a <pre> tag.
console: Typing
<% console %> inside an ERB template
will trigger the Web Console in your browser when that
page renders.
Warning: Never leave
binding.break, console, or
detailed logger.debug statements in your code
when pushing to production. They
can cause your application to hang or leak sensitive data
into the logs.
Note
For performance-related debugging, use the
SQL logging in your
terminal. If you see dozens of identical queries
(e.g.,
SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ?),
you have an N+1 query problem that
should be solved with
.includes.
Configuring Rails Applications
Rails follows the "Convention over Configuration" philosophy,
but every application
eventually requires custom settings for different
environments (Development, Test, and
Production). Configuration in Rails is centralized,
primarily residing in the
config/ directory.
The Configuration Hierarchy
Rails loads configuration in a specific order. Settings
defined in environment-specific files
override the general settings defined in
application.rb.
| File |
Scope |
Purpose |
config/application.rb |
Global |
Settings that apply to all environments
(e.g., time zone, autoload paths).
|
config/environments/*.rb |
Environment-Specific |
Settings for development,
test, or
production (e.g., caching, log
levels).
|
config/initializers/*.rb |
Component-Specific |
Code executed after the framework and gems
are loaded (e.g., API keys,
devise config). |
Environment-Specific Settings
Most of your configuration changes will happen in the
environment files to ensure the app
behaves appropriately for its context.
# config/environments/production.rb
Rails.application.configure do
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
# Log to STDOUT (useful for Docker/Heroku)
config.log_formatter = ::Logger::Formatter.new
# Set the log level
config.log_level = :info
end
Managing Secrets (Credentials)
Rails uses a built-in Encrypted Credentials
system to store sensitive
information like API keys and database passwords. This
allows you to check your credentials
into version control securely, as they are encrypted with
a master.key.
- Edit Credentials: Run
bin/rails credentials:edit. This
opens your system's default editor.
- Access Credentials:
# Accessing a nested key in your code
stripe_key = Rails.application.credentials.dig(:stripe, :api_key)
Initializers
Initializers are Ruby files located in
config/initializers/. They run once when
the application boots. Use these for settings that don't fit
into the standard environment
files or for configuring third-party gems.
# config/initializers/time_formats.rb
# Define a custom date format used app-wide
Date::DATE_FORMATS[:short_date] = "%d %b %Y"
Common Configuration Tasks
| Task |
Configuration Location |
Example |
| Time Zone |
application.rb |
config.time_zone = 'Eastern Time (US & Canada)'
|
| Locale (I18n) |
application.rb |
config.i18n.default_locale = :es
|
| Database |
database.yml |
Setting host, username, and pool size. |
| Asset Precompilation |
production.rb |
config.assets.compile = false
|
Custom Configuration
You can also add your own custom configuration settings to
the Rails object, making them
accessible throughout your application.
# config/application.rb
config.x.payment_gateway.merchant_id = "MID_12345"
# Accessing it in a controller
merchant_id = Rails.configuration.x.payment_gateway.merchant_id
Warning: Never commit your
config/master.key to a git
repository. If this key is lost or stolen, anyone can
decrypt your production secrets, and
you will lose access to your own encrypted data.
\
Note
After changing any file in
config/initializers/ or
config/environments/, you must
restart your Rails
server for the changes to take effect.
The Command Line Tools
The Rails Command Line Interface (CLI) is the engine room of
development. It follows the
principle of "Generators and Tasks,"
allowing you to automate repetitive
coding chores and manage your application's lifecycle
without leaving the terminal.
Core Commands
Most Rails commands follow the pattern
bin/rails <command> [options].
Using bin/rails ensures you are using the
specific version of Rails bundled
with your current project.
| Command |
Shortcut |
Purpose |
bin/rails server |
s |
Launches the local web server (usually at
localhost:3000).
|
bin/rails console |
c |
Opens an interactive Ruby session with your
app environment loaded. |
bin/rails generate |
g |
Creates new code stubs (models, controllers,
migrations). |
bin/rails db:... |
N/A |
Performs database operations (migrate, seed,
setup). |
bin/rails routes |
N/A |
Lists all URL patterns and their
corresponding controller actions. |
bin/rails test |
t |
Runs your test suite. |
The Power of Generators
Generators are blueprints that create files based on Rails
conventions. They don't just
create files; they also update your routes and create test
stubs.
- Model Generator:
bin/rails g model Article title:string body:text
- Creates a migration file and the model file.
- Controller
Generator:
bin/rails g controller Products index show
- Creates the controller, views for index and show,
and adds routes.
- Scaffold
Generator:
bin/rails g scaffold Task name:string status:integer
- The "All-in-One": Generates the
model, migration, controller,
views, tests, and CSS for a full CRUD interface. Use
this sparingly in real
projects, as it generates a lot of "boilerplate"
code.
Database Tasks
The db: namespace is critical for managing your
schema and data state.
| Task |
Action |
bin/rails db:create |
Creates the database defined in
database.yml.
|
bin/rails db:migrate |
Runs pending migrations to update your
schema. |
bin/rails db:rollback |
Reverts the last migration performed. |
bin/rails db:seed |
Loads the data defined in
db/seeds.rb into the database.
|
bin/rails db:prepare |
A "catch-all" that runs create and migrate
(perfect for first-time setup).
|
Specialized Tools
As your app grows, you will use specific tools for assets and
background processing.
bin/dev:L If you use a CSS/JS bundler (like
Tailwind or esbuild), this
command starts the Rails server and the asset watchers
simultaneously.
bin/bundle: Manages your Gemfile
dependencies.
bin/rails notes: Searches through your code
for comments like
TODO, FIXME, or
OPTIMIZE.
Best Practices
- Dry Run: Add
-d or
--pretend to a generate
command to see what files will be created without
actually creating them.
- Destroy: If you made a mistake in a
generator, use
bin/rails destroy <type> <name>
(e.g.,
bin/rails d model Article) to undo all
files created by that specific
generator.
- Shortcuts: While
bin/rails
is standard, many developers
use rails directly. However, using
bin/rails is safer for
ensuring version consistency across different projects
on the same machine.
Note
You can see a full list of available tasks by
running bin/rails --help
or bin/rails -T.
Testing Rails Applications (Introduction)
Testing is not an "add-on" in the Rails ecosystem; it is a core philosophy. Rails was
built with testing in mind from day one, providing a robust infrastructure to ensure
that your application behaves as expected as it grows. The goal of testing is to provide
a "safety net" so you can refactor code, upgrade gems, or add features without breaking
existing functionality.
Why Test in Rails?
- Prevent Regressions: Ensure that fixing one bug doesn't introduce
another.
- Documentation: Tests serve as a living technical specification of
what the code is supposed to do.
- Confidence: Allows for "Continuous Deployment" where code can be
shipped to production automatically if the tests pass.
The Rails Testing Environment
Rails automatically creates a test directory when you generate a new
project. It also maintains a separate Test Database so your testing
process doesn't interfere with your development data.
| Component |
Purpose |
test/models |
Logic for data validation and business rules. |
test/controllers |
Testing request handling, redirects, and responses. |
test/integration |
Testing the interaction between multiple controllers/models. |
test/system |
Full browser automation (testing the UI like a real user). |
test/fixtures |
Sample data used to populate the test database. |
Types of Tests in Rails
| Test Level |
Target |
Speed |
Focus |
| Unit Tests |
Models |
Fast |
Validations, scopes, and logic. |
| Functional Tests |
Controllers |
Medium |
Status codes, parameters, and redirects. |
| Integration Tests |
Full Workflow |
Slow |
A user logging in and posting an article. |
| System Tests |
Browser/UI |
Very Slow |
JavaScript execution and CSS visibility. |
Anatomy of a Test
Rails uses Minitest by default (though RSpec is a
popular community alternative). A standard test follows the AAA
pattern: Arrange (setup data), Act (run the code), and
Assert (check the result).
# test/models/article_test.rb
require "test_helper"
class ArticleTest < ActiveSupport::TestCase
test "should not save article without title" do
# 1. Arrange
article = Article.new
# 2. Act & 3. Assert
assert_not article.save, "Saved the article without a title"
end
end
Running Tests
You can run your tests through the command line. Rails provides detailed feedback on
which tests passed, failed, or errored.
bin/rails test: Runs all tests in the test folder.
bin/rails test test/models/article_test.rb: Runs tests for a specific
file.
bin/rails test:system: Runs only the slow-running browser/system tests.
Fixtures: The Test Data
Instead of manually creating records for every test, Rails uses
Fixtures. These are YAML files located in test/fixtures/
that define the initial state of your test database.
# test/fixtures/articles.yml
first_article:
title: "My First Post"
body: "This is some content."
second_article:
title: "A Secret Post"
body: "Confidential info."
Note
When you run a test, Rails automatically loads these fixtures into the test
database and wraps each test in a Database Transaction. This
means any changes made during the test are "rolled back" afterward, ensuring a
clean state for the next test.
Warning: Avoid "Testing Implementation Details." Focus your tests on the
outcome (e.g., does the user see a success message?) rather than the specific internal
method name used to get there.
System Testing
System Tests (also known as End-to-End or Acceptance tests) occupy the
top of the testing pyramid. While Model and Controller tests check individual "gears" of
the machine, System Tests check theentire machine by automating a real
browser. They simulate a user’s journey—clicking buttons, filling out forms, and
verifying that JavaScript and CSS are functioning correctly.
In Rails, System Tests are powered by Capybara, which provides a
human-readable DSL (Domain Specific Language) to interact with the browser.
Why System Testing is Essential
| Feature |
Benefit |
| JavaScript Execution |
Verifies features like modals, dropdowns, and Hotwire/Turbo updates that
standard controller tests cannot see. |
| Visual Verification |
Ensures elements are actually visible and clickable on the screen. |
| User Flow |
Validates that the "Happy Path" (e.g., Sign Up -> Purchase ->
Receipt) works from start to finish. |
| Cross-Browser |
Can be configured to run against Chrome, Firefox, or Safari to catch
browser-specific bugs. |
Configuration: test_helper.rb
System tests are configured in test/application_system_test_case.rb. You can
choose between "headless" browsers (fast, no UI) or "headed" browsers (opens a window so
you can watch the test run).
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# :headless_chrome is faster for CI/CD pipelines
# :chrome is better for debugging so you can see the actions
driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
end
Writing a System Test
You can generate a system test using:
bin/rails generate system_test Users.
# test/system/articles_test.rb
require "application_system_test_case"
class ArticlesTest < ApplicationSystemTestCase
test "creating a new article" do
visit articles_path
click_on "New Article"
fill_in "Title", with: "Learning Rails"
fill_in "Body", with: "System tests are powerful!"
click_on "Create Article"
# Assertions check the actual rendered content in the browser
assert_text "Article was successfully created"
assert_selector "h1", text: "Learning Rails"
end
end
Common Capybara Commands
Capybara allows you to interact with the page as a user would, using simple English-like
methods.
| Action Type |
Commands |
| Navigation |
visit root_path, go_back |
| Interaction |
click_on "Save", fill_in "Email", with: "...",
check "Remember Me"
|
| Discovery |
find(".profile-link"), first(:link, "Edit")
|
| Assertions |
assert_selector, assert_no_text,
assert_button_enabled
|
| Debugging |
take_screenshot, save_and_open_page |
Comparison: System Tests vs. Integration Tests
| Feature |
System Tests |
Integration Tests |
| Engine |
Full Browser (via Selenium/Cuprite) |
Rack-Test (Fast HTTP simulation) |
| JavaScript |
Fully Supported |
Not Supported |
| Speed |
Slow (High overhead) |
Fast (Low overhead) |
| Use Case |
Complex UI and User Journeys |
API endpoints and simple form posts |
Screenshots and Debugging
System tests are notoriously difficult to debug when they fail because the browser state
is ephemeral. Rails assists by:
- Automatic Screenshots: If a system test fails, Rails automatically
saves a screenshot of the browser at the exact moment of failure in
tmp/screenshots.
- Pause for Inspection: You can insert
binding.break in
your test code to pause the browser window, allowing you to manually inspect the
HTML or CSS.
Warning: Because system tests are slow, avoid writing them for every
minor edge case. Use them for your primary user flows and rely on faster Model/Unit
tests for logic-heavy edge cases.
Note
For modern Rails apps using Hotwire, system tests are your best
friend. Since Hotwire relies on partial DOM updates via WebSockets or AJAX,
system tests are often the only way to verify those updates occur correctly.
Integration Testing
Integration tests sit between fast Unit tests and slow System tests. While a Model test
checks a single class and a System test checks the entire browser experience, an
Integration Test focuses on how different parts of your application
(controllers, models, and routes) interact during a request-response cycle.
In Rails, integration tests use the Rack::Test library, which simulates HTTP
requests extremely quickly without the overhead of a real browser or JavaScript engine.
The Purpose of Integration Tests
Integration tests are ideal for verifying that your "business workflows" are intact. They
ensure that a request hits the right route, the controller manipulates the model
correctly, and the user is redirected to the proper destination.
| Feature |
Integration Testing Benefit |
| Performance |
Much faster than System tests because they don't load a browser. |
| Workflow Logic |
Can follow multiple redirects and maintain session state across
requests. |
| Security |
Excellent for testing access control (e.g., ensuring a guest cannot
access an admin page). |
| API Testing |
The standard way to test JSON/REST API endpoints. |
Creating an Integration Test
Generate an integration test using:
bin/rails generate integration_test UserFlows.
# test/integration/user_flows_test.rb
require "test_helper"
class UserFlowsTest < ActionDispatch::IntegrationTest
test "can see the homepage and create an article" do
# 1. Navigate to the index
get "/"
assert_response :success
# 2. Post data to the create action
post "/articles", params: { article: { title: "New Blog", body: "Content here" } }
# 3. Follow the redirect to the show page
follow_redirect!
assert_response :success
assert_select "h1", "New Blog"
end
end
Key Integration Testing Methods
Because integration tests simulate a web client, they provide methods to mimic browser
behavior and inspect the resulting server state.
| Method |
Description |
get, post, patch,
delete
|
Simulates an HTTP request to a specific path. |
assert_response |
Checks the HTTP status (e.g., :success,
:redirect, :forbidden).
|
follow_redirect! |
Tells the test runner to follow the HTTP 302 redirect to the next page.
|
assert_select |
Uses CSS selectors to verify specific HTML elements are present in the
response. |
host! |
Allows you to simulate requests coming from a specific domain or
subdomain. |
Integration vs. Controller Tests
In modern Rails (since version 5), Controller TestsM have largely been
superseded by Integration Tests. While you can still write specific
controller tests, the Rails team recommends using integration tests for most
"request-level" testing.
| Aspect |
Integration Test (Recommended) |
Controller Test (Legacy Style) |
| Scope |
Full stack (Routes -> Controller -> Model) |
Controller instance only. |
| Execution |
Real HTTP simulation via Rack. |
Direct method calls on controller actions. |
| Redirection |
Can follow redirects automatically. |
Cannot follow redirects; only checks headers. |
Testing APIs
Integration tests are the primary tool for testing Rails APIs. Since APIs don't have a UI
for System Tests to interact with, you use integration tests to verify JSON structures
and status codes.
test "api returns list of products" do
get "/api/v1/products", as: :json
assert_response :success
json_response = JSON.parse(response.body)
assert_equal 3, json_response.length
assert_equal "Laptop", json_response.first["name"]
end
Warning:Integration tests cannot test JavaScript. If your feature
depends on a React component, a Stimulus controller, or a jQuery script to function, you
must use a System Test instead.
Note
Use assert_select sparingly. If you change your CSS classes
frequently, your integration tests will break even if the logic is correct.
Focus on asserting the presence of data rather than specific styling.
The Basics of Creating Rails Plugins
A Rails Plugin (often distributed as a Gem) is a way to
package and share code across multiple Rails applications. Whether you want to add a
custom authentication method, a set of UI helpers, or a new database adapter, plugins
allow you to "extend" the core functionality of Rails without cluttering your main
application logic.
In modern Rails, plugins are almost exclusively built as Rails Engines.
An Engine can be thought of as a "miniature application" that provides its own routes,
controllers, models, and views to a host application.
Types of Extensions
Depending on your goal, there are three primary ways to extend Rails:
| Extension Type |
Best For |
Examples |
| Simple Gem |
Pure Ruby logic or standalone utilities. |
nokogiri, httparty |
| Railtie |
Deep integration with the Rails boot process (initializers, generators).
|
dotenv-rails, debug |
| Engine |
Full-stack features including routes, models, and views. |
devise, active_admin, thredded
|
Creating a New Plugin
Rails provides a specialized generator to scaffold a plugin project. Using the
--mountable flag creates a "Mountable Engine," which stays isolated in its
own namespace to prevent naming conflicts with the host app.
# Generate a new mountable engine named 'blog_engine'
bin/rails plugin new blog_engine --mountable
This command generates a directory structure that looks remarkably like a standard Rails
app, including:
lib/: The core Ruby code for your plugin.
app/: Models, controllers, and views specific to the plugin.
config/routes.rb: Isolated routes for the engine.
test/: A "dummy" Rails application used to test the plugin.
Key Component: The Engine Class
The engine.rb file is the heart of your plugin. It inherits from
Rails::Engine and allows you to hook into the host application's lifecycle.
# blog_engine/lib/blog_engine/engine.rb
module BlogEngine
class Engine < ::Rails::Engine
isolate_namespace BlogEngine
# Use an initializer to set up configuration
initializer "blog_engine.configure_rails_initialization" do |app|
# Custom setup logic here
end
end
end
Integrating with the Host Application
To use your plugin in a real project, you add it to the host's Gemfile and
"mount" it in the routes.
1. The Gemfile
gem 'blog_engine', path: 'vendor/plugins/blog_engine'
2. The Routes
# config/routes.rb in the Host App
Rails.application.routes.draw do
# All blog engine routes will now start with /blog
mount BlogEngine::Engine => "/blog"
end
Best Practices for Plugin Development
- Namespace Everything: Always wrap your models and controllers in a
module (e.g.,
BlogEngine::Post) to avoid overwriting a host app's
Post model.
- Keep it Modular: Provide configuration options so the host app can
choose how the plugin behaves (e.g., changing the default table name).
- Test Against the Dummy App: Use the generated
test/dummy application to ensure your engine works correctly within a
real Rails environment.
Note
If you only need to add a single method to a core Rails class
(like ActiveRecord::Base), you might not need a full Engine. You
can use a ActiveSupport::Concern or a Railtie
to inject code during the boot process
Warning: Be careful when overriding core Rails methods in a plugin
(Monkey Patching). If Rails updates its internal logic, your plugin might break the host
application in unexpected ways.
Rails Engines
A Rails Engine is a subset of the Rails framework that allows you to
wrap a full-featured Rails application within another. In fact, every Rails application
is essentially a "super engine," with Rails itself providing the core functionality
through engines like ActiveRecord and ActionController.
Mountable vs. Full Engines
When creating an engine, you must decide how deeply integrated it should be with the host
application.
| Feature |
Full Engine |
Mountable Engine (Recommended) |
| Namespacing |
Shares the host's namespace. |
Isolated namespace (e.g., Blog::Post). |
| Routes |
Merged into the host's routes. |
Mounted at a specific path (e.g., /blog). |
| Assets |
Shared with the host. |
Isolated CSS/JS manifests. |
| Best For |
Extending existing models/logic. |
Creating modular, plug-and-play features. |
Key Characteristics of Mountable Engines
Mountable engines are the most common way to build modular Rails components because they
provide Isolation.
- Isolated Namespace: By calling
isolate_namespace in
the engine class, the engine’s models and controllers won't conflict with those in
the host app. A User model in the engine becomes
MyEngine::User.
- Independent Routing: The engine has its own
routes.rb.
The host application "mounts" the engine at a specific endpoint, and all internal
links remain relative to that endpoint.
- The Dummy App: Every engine comes with a
test/dummy
directory. This is a tiny Rails app used to test the engine in a "real-world"
scenario during development.
Implementation: Isolation in Action
To ensure your engine doesn't break when used in different apps, you must use specific
helpers to link between the engine and the host.
# To link to a page WITHIN the engine:
# (Uses the engine's internal route helper)
<%= link_to "Engine Home", blog_engine.root_path %>
# To link to a page in the HOST application:
# (Uses the main_app helper)
<%= link_to "Back to Main Site", main_app.root_path %>
Why Use Rails Engines?
Engines are powerful tools for managing large-scale applications (often referred to as
Component-Based Rails Architecture or CBRA).
- Code Reusability: Build a "Forum" engine once and mount it in ten
different client apps.
- Team Isolation: Large teams can work on separate engines (e.g.,
Billing, Inventory, User Profile) without stepping on each other's code.
- Faster Testing: You can run tests for a single engine in seconds
rather than waiting for a massive monolithic test suite to finish.
- Gradual Migration: If you are moving toward a microservices
architecture, you can first extract logic into an Engine before moving it to a
completely separate server.
Common Examples of Engines
Many of the most popular gems in the Rails ecosystem are actually Engines:
- Devise: A full authentication system with its own views and
controllers.
- Sidekiq Web: The dashboard for monitoring background jobs.
- Active Admin: A complete administration interface that you mount
at
/admin.
Warning: While engines provide great isolation, sharing data between the
host and the engine (like a User model) can be tricky. It is often best to
define a "User" interface or use a specific configuration block to tell the engine which
class represents the user in the host app.
Active Support Core Extensions
Active Support is the component of Rails responsible for providing Ruby
language extensions, utilities, and other transversal concerns. While most of Rails
focuses on web-specific logic (MVC), Active Support enriches the Ruby "Core" (classes
like String, Array, and Time) with "sugar" that
makes the code more readable and expressive.
This philosophy is often called Monkey Patching—Active Support reopens
Ruby’s built-in classes and adds new methods to them.
1. Extensions to Time and Date
This is perhaps the most famous part of Active Support. It allows for natural-language
time calculations that read like English sentences.
| Method |
Example |
Result (if today is Jan 1) |
| Numeric helpers |
2.days, 1.week, 3.hours |
Returns an ActiveSupport::Duration |
| Relative time |
3.days.from_now |
Jan 4 |
| Past time |
10.minutes.ago |
10 minutes before now |
| Calendrical |
Date.today.beginning_of_month |
Jan 1 |
| Advanced |
5.years.ago.midnight |
Jan 1, five years ago at 00:00 |
2. Extensions to String
Active Support adds powerful transformation methods to strings, which Rails uses
internally to convert between class names, table names, and URLs.
pluralize / singularize: "person".pluralize ?
"people"
camelize / underscore:
"user_profile".camelize ? "UserProfile"
parameterize: "Rails is Awesome!".parameterize ?
"rails-is-awesome" (perfect for slugs)
truncate: "Very long text...".truncate(10) ? "Very lo..."
squish: Removes all surrounding and internal duplicate whitespace.
3. Extensions to Array and Hash
These extensions make data manipulation significantly more concise.
| Method |
Class |
Description |
to_sentence |
Array |
['A', 'B', 'C'].to_sentence →
"A, B, and C"
|
in_groups_of |
Array |
Splits array into chunks (useful for grid layouts). |
except |
Hash |
Returns a hash without the specified keys. |
stringify_keys |
Hash |
Converts all keys to strings. |
deep_merge |
Hash |
Merges nested hashes recursively. |
4. The "Object" Extensions: blank? and
present?
In standard Ruby, checking if something exists requires multiple checks
(nil?,empty?, etc.). Active Support adds blank?
to every object.
blank?: Returns true if the object
is nil,false, an empty string, an empty array/hash, or a
string consisting only of whitespace.
present?: The exact opposite of blank?.
presence: Returns the object if it's present?, otherwise
returns nil.
- Example:
name = params[:name].presence || "Guest"
5. ActiveSupport::Concern
When you want to extract common logic into a module to be shared across multiple models
or controllers, Concerns provide a standardized way to handle both
instance methods and class methods (like validations or scopes) in one place.
# app/models/concerns/taggable.rb
module Taggable
extend ActiveSupport::Concern
included do
# Code here runs in the context of the class it's included in
has_many :tags
scope :with_tags, -> { joins(:tags) }
end
# Instance methods go here
def tag_list
tags.map(&:name).join(", ")
end
end
6. Instrumentation and Notifications
Active Support includes a pub/sub system called
ActiveSupport::Notifications. You can "subscribe" to events (like a
database query or a controller action) to track performance or log custom data.
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
puts "Action took #{event.duration}ms"
end
Warning: Be careful when using Active Support in a standalone Ruby
script. Because it
modifies core classes, it can lead to conflicts if other libraries expect standard Ruby
behavior. In a Rails app, however, this is considered idiomatic.
Note
If you are building a small gem and don't want to load all of Active Support, you
can cherry-pick specific
extensions: require 'active_support/core_ext/object/blank'.
ActionController API
ActionController::Base is the heart of the web request cycle. It acts as the
"Director," taking data from the router, communicating with models, and rendering views
or redirecting. Most of your controllers will inherit from
ApplicationController, which in turn inherits from
ActionController::Base.
1. Request Handling & Parameters
Controllers interpret incoming data through the params hash, which combines
query string parameters, POST data, and route segments.
| Method |
Purpose |
Example |
params |
Accesses all request parameters. |
params[:id] |
request |
Accesses the full Request object. |
request.remote_ip, request.user_agent |
headers |
Accesses or sets HTTP headers. |
response.headers["X-Frame-Options"] = "SAMEORIGIN" |
session |
Accesses the user's session (encrypted cookies). |
session[:user_id] = user.id |
cookies |
Manages browser cookies. |
cookies.signed[:prefs] = "dark-mode" |
2. The Filter Lifecycle (Callbacks)
Filters are methods that run before, after, or around a controller action. They are
primarily used for authentication, authorization, or setting up data.
before_action: Runs before the action. If it renders or redirects, the
action is not executed.
after_action: Runs after the action. Useful for logging or
post-processing headers.
around_action: Yields to the action. Useful for wrapping actions in
database transactions or error handling.
class ArticlesController < ApplicationController
# Only runs 'set_article' for show, edit, update, and destroy
before_action :set_article, only: [:show, :edit, :update, :destroy]
private
def set_article
@article = Article.find(params[:id])
end
end
3. Rendering and Redirecting
A controller action must eventually result in a response. If you don't call one of these
methods, Rails attempts to render a template matching the action name.
| Method |
Behavior |
Syntax |
render |
Renders a view template or raw data. |
render :edit, render json: @user |
redirect_to |
Sends a 302 redirect to the browser. |
redirect_to articles_path, notice: "Saved!" |
head |
Returns an empty response with a status code. |
head :no_content, head :unauthorized |
send_data |
Sends binary data (like a generated PDF). |
send_data pdf_blob, filename: "report.pdf" |
4. Strong Parameters
To prevent "Mass Assignment" vulnerabilities, Rails requires you to explicitly whitelist
which attributes are allowed to be passed to your models.
def article_params
# Permits only 'title' and 'body'
params.require(:article).permit(:title, :body)
end
# Usage:
# Article.create(article_params)
5. Built-in Helpers & Scopes
Action Controller provides several utility methods to manage the flow of data to the
view.
helper_method: Makes a controller method available to use inside your
views.
flash: A special part of the session that clears after the next request
(used for "Success" or "Error" messages).
respond_to: Allows one action to return different formats (HTML, JSON,
XML) based on the request.
def index
@articles = Article.all
respond_to do |format|
format.html # renders index.html.erb
format.json { render json: @articles }
end
end
6. Live Streaming
For features like server-sent events or real-time progress bars, you can use
ActionController::Live.
class MyController < ActionController::Base
include ActionController::Live
def stream
response.headers['Content-Type'] = 'text/event-stream'
10.times do |i|
response.stream.write "Iteration #{i}\n"
sleep 1
end
ensure
response.stream.close
end
end
Warning: Remember that render and redirect_to
do not end the execution of your method. They simply set the response. If you have code
after these calls, it will still execute unless you explicitly return.
ActiveRecord API
Active Record is the Object-Relational Mapping (ORM) layer of Rails. It
translates complex SQL queries into intuitive Ruby methods and handles the persistence
of data between your Ruby objects and the relational database.
1. Finder Methods (Querying)
Active Record uses "lazy loading," meaning the database isn't queried until the data is
actually needed. This allows you to chain multiple methods together efficiently.
| Method |
SQL Equivalent |
Description |
find(id) |
WHERE id = ? |
Finds a record by ID; raises ActiveRecord::RecordNotFound
if missing. |
find_by(...) |
WHERE ... LIMIT 1 |
Finds the first record matching criteria; returns nil if
missing. |
where(...) |
WHERE ... |
Returns an ActiveRecord::Relation (a collection) matching
criteria. |
order(:col) |
ORDER BY col |
Sorts the results (default is ASC). |
limit(n) |
LIMIT n |
Constrains the number of records returned. |
pluck(:col) |
SELECT col |
Returns an array of values instead of object instances. |
2. Persistence (Saving and Updating)
These methods handle the creation, modification, and deletion of records.
save: Saves the model. Returns true on success,
false on failure.
create: Instantiates and saves a record in one step.
destroy: Removes the record from the database and triggers "destroy"
callbacks.
delete: Removes the record directly from the database (fast, but skips
callbacks).
3. Advanced Querying & Aggregates
Active Record provides Ruby wrappers for complex SQL operations.
# Aggregates
Article.count
Article.average(:word_count)
Article.maximum(:views)
# Chaining with Scopes
# SQL: SELECT * FROM articles WHERE status = 'published' ORDER BY created_at DESC LIMIT 5
Article.published.order(created_at: :desc).limit(5)
4. The "N+1" Solver: Eager Loading
To avoid the common performance pitfall where a query is executed for every child record
in a loop, use eager loading.
| Method |
Behavior |
Use Case |
includes |
Loads associations in separate queries or a JOIN. |
General-purpose eager loading. |
joins |
Uses an INNER JOIN. |
Filtering results based on association values. |
left_outer_joins |
Uses a LEFT OUTER JOIN. |
Including records that may not have an association. |
5. Scopes
Scopes allow you to define commonly used queries as class methods inside your model for
better readability and reusability.
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
end
# Usage:
@articles = Article.published.recent
6. Calculations and Groups
You can perform complex grouping and counting directly through the API.
# Returns a Hash: { "Category A" => 5, "Category B" => 12 }
Article.group(:category).count
7. Raw SQL (The Escape Hatch)
When Active Record's API isn't enough, you can still execute raw SQL while maintaining
safety against injections.
find_by_sql: Returns model instances from a custom string.
ActiveRecord::Base.connection.execute: Executes raw SQL and returns a
result set (not model objects).
Warning: Always use the hash syntax where(name: val) or the
array syntax where("name = ?", val) to prevent SQL
Injection. Never interpolate variables directly into a string:
where("name = #{val}").
Note
Use find_each instead of each when iterating over
thousands of records. find_each batches the records (1,000 at a
time) to prevent your application's memory from spiking.
ActiveModel API
Active Model is a library that contains various modules used in Active
Record to provide integration with the Rails framework. However, its true power lies in
its ability to be used outside of database-backed models. It allows you to create "Plain
Old Ruby Objects" (POROs) that behave like Rails models, making them compatible with
form helpers, validations, and serialization.
1. Making a Class "Rails-Ready"
By including ActiveModel::Model, your Ruby class gains features like
attribute initialization, validations, and conversion for use in form_with.
class ContactForm
include ActiveModel::Model
include ActiveModel::Attributes # For typed attributes
attribute :name, :string
attribute :email, :string
attribute :message, :string
validates :name, :email, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
def submit
if valid?
# Send email logic here
true
else
false
end
end
end
2. Core Modules and Their Functions
Active Model is modular; you can include only what you need.
| Module |
Purpose |
Example Method |
ActiveModel::Validations |
Adds the ability to validate attributes. |
valid?, errors |
ActiveModel::Attributes |
Adds support for data types (string, integer, etc.). |
attribute :price, :decimal |
ActiveModel::Dirty |
Tracks changes to attributes before saving. |
name_changed?, name_was |
ActiveModel::Serialization |
Converts objects to/from JSON or XML. |
as_json |
ActiveModel::Callbacks |
Provides Active Record-style lifecycle hooks. |
define_model_callbacks |
ActiveModel::Naming |
Provides routing helpers (e.g., model_name). |
ContactForm.model_name.human |
3. Dirty Tracking (Change Tracking)
This module allows you to see what has changed in an object since it was last "synced."
This is useful for audit logs or conditional logic.
person = Person.new
person.name = "Alice"
person.name_changed? # => true
person.name_change # => [nil, "Alice"]
person.name_was # => nil
person.restore_name! # Resets name to previous value
4. Serialization
Active Model provides a standard way to convert your object into a hash for API
responses.
class User
include ActiveModel::Serialization
attr_accessor :name, :email
def attributes
{'name' => nil, 'email' => nil}
end
end
user = User.new(name: "Bob", email: "bob@example.com")
user.serializable_hash # => {"name"=>"Bob", "email"=>"bob@example.com"}
5. Attribute Methods
This module allows you to add custom prefixes or suffixes to all your attributes
dynamically, similar to how Rails adds _changed?.
class Person
include ActiveModel::AttributeMethods
attribute_method_suffix '_verify'
define_attribute_methods :name
def name_verify
# custom logic
end
end
6. Errors Object
When validations fail, Active Model populates an errors object. This is a
sophisticated collection that handles internationalization (I18n) and message
formatting.
errors.add(:base, "Message"): Adds a general error not tied to a
specific field.
errors.full_messages: Returns an array of user-friendly strings (e.g.,
"Email is invalid").
errors[:email]: Returns an array of specific errors for the email
field.
Note
Use ActiveModel::Model whenever you have a form that doesn't save
directly to a database table (e.g., Search filters, Login forms, or Password
resets). This keeps your controllers clean and your views consistent.
Warning: While Active Model provides the behavior of a model, it does
not provide persistence. Methods like save or update do not
exist unless you define them yourself.
ActiveSupport API
Active Support is the "utility belt" of the Rails framework. While other
components focus on the Web or the Database, Active Support focuses on the Ruby language
itself. It provides extensions to the Ruby Core and a collection of tools for
internationalization, caching, and background processing.
1. Core Extensions (Language Sugar)
Active Support reopens standard Ruby classes to add methods that make code more
expressive. These are often called "Core Extensions."
| Target Class |
Popular Methods |
Example |
| Object |
blank?, present?, presence,
try
|
user.try(:name) |
| String |
camelize, underscore, truncate,
pluralize
|
"admin_user".camelize |
| Array |
to_sentence, in_groups_of,
second, forty_two
|
names.to_sentence |
| Hash |
deep_merge, except,
stringify_keys, slice
|
params.slice(:id, :name) |
| Numeric |
bytes, kilobytes, megabytes |
5.megabytes |
2. Time & Duration API
Perhaps the most iconic part of Active Support, this API allows for human-readable time
manipulation.
Duration: Objects like 1.month or 30.minutes
that can be added to or subtracted from Time objects.
- Travel: Methods like
2.days.ago or
1.week.from_now.
- Calculations:
Time.current.beginning_of_year or
Date.yesterday.
- Zones:
Time.zone.now (Uses the time zone set in
application.rb rather than the server's system time).
3. ActiveSupport::Concern
This module streamlines the "Mix-in" pattern in Ruby. It handles the boilerplate of
including instance methods and extending class methods simultaneously, while also
managing dependencies between multiple modules.
module Mailable
extend ActiveSupport::Concern
included do
# Runs in the class context (e.g., validations, scopes)
validates :email, presence: true
end
class_methods do
# Defines methods on the class itself
def find_by_email(email)
find_by(email: email)
end
end
end
4. Instrumentation (Notifications)
Active Support provides a internal "Pub/Sub" (Publisher/Subscriber) system. Rails uses
this to track how long database queries or controller actions take.
instrument: Wraps a block of code to measure it and notify subscribers.
subscribe: Listens for specific events to log data or trigger side
effects.
# Subscribing to any SQL query
ActiveSupport::Notifications.subscribe "sql.active_record" do |name, started, finished, unique_id, payload|
puts "Query: #{payload[:sql]}"
end
5. Caching (ActiveSupport::Cache)
Active Support provides a unified interface for different caching stores (Memory, File,
Memcached, Redis).
| Store |
Configuration Example |
Best Use Case |
| Memory |
:memory_store |
Small, single-process apps. |
| File |
:file_store |
Low-traffic apps with limited RAM. |
| Redis |
:redis_cache_store |
Large, multi-server production environments. |
Standard Usage:
# Fetches the value from cache, or runs the block and saves it if missing
Rails.cache.fetch("expensive_data_key", expires_in: 12.hours) do
ExpensiveCalculation.run
end
6. JSON & Security Utilities
ActiveSupport::JSON: High-performance JSON encoding and decoding.
ActiveSupport::MessageVerifier: Generates and verifies signed messages
(used for "Remember Me" cookies).
ActiveSupport::EncryptedConfiguration: The engine behind the
credentials.yml.enc system.
7. Callbacks
Active Support allows you to define your own lifecycle hooks for any Ruby object, similar
to before_save in Active Record.
class Goal
extend ActiveSupport::Callbacks
define_model_callbacks :complete
def complete
run_callbacks :complete do
puts "Goal marked as finished!"
end
end
end
Note
Active Support is extremely modular. If you are writing a gem or a standalone
script, you can load only the extensions you need
(e.g., require 'active_support/core_ext/integer/time') to keep the
memory footprint low.
Warning: Be wary of Monkey Patching conflicts. If you
use Active Support in a project with other libraries that also modify core classes (like
Hash or String), ensure their implementations don't overwrite
each other.
Railties API
Content goes here...