Table of Contents 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 1. Introduction What Is Ruby on Rails? Ruby on Rails (usually just called "Rails") is a server side web ap…
Table of Contents
- Introduction
- Setting Up Your Environment
- Creating Your First Rails App
- Routing
- Controllers and Actions
- Models and ActiveRecord
- Views and ERB Templates
- Worked Example: A Task List App
- Testing Basics
- Deployment Basics
- Next Steps and Further Resources
1. Introduction
What Is Ruby on Rails?
Ruby on Rails (usually just called "Rails") is a server-side web application
framework written in the Ruby programming language. It was created by David
Heinemeier Hansson and first released in 2004, and it has powered
products like Shopify, GitHub (in its early years), Basecamp, and countless
startups and internal business tools ever since.
Rails is what's called a full-stack framework: it gives you a database
layer, a way to define URLs and behavior, a templating system for HTML, and
conventions for testing, background jobs, file uploads, and more — all out
of the box. Compare this to a "microframework" like Sinatra or Express.js,
which gives you routing and not much else, leaving you to assemble the rest
yourself.
Why Developers Still Choose Rails
As of 2026, Rails has settled comfortably into a specific and very useful
niche: database-backed web applications built by small-to-medium teams
that want to move fast without reinventing infrastructure. It's not the
trendiest framework, but it is mature, stable, well-documented, and
extremely productive once you understand its conventions.
The core selling points are:
- Convention over Configuration (CoC). Rails makes decisions for you.
A model called Product maps to a database table called products, is
defined in app/models/product.rb, and so on. You rarely need to write
configuration files to wire things together — you just follow the naming
conventions and Rails figures out the rest.
- Don't Repeat Yourself (DRY). Rails avoids having you specify the same
information in multiple places (for example, database schema information
doesn't need to be duplicated in your model code).
- Batteries included. Routing, an ORM (ActiveRecord), a templating
engine (ActionView / ERB), background job processing (ActiveJob), email
(ActionMailer), file uploads (ActiveStorage), and a testing framework are
all part of the box.
- A mature ecosystem. Thousands of gems (Ruby libraries) exist for
almost anything you'd want to add — payments, search, admin panels,
authentication, and more.
The MVC Pattern
Rails is built around the Model-View-Controller (MVC) architectural
pattern. Understanding this pattern is the single most important mental
model for learning Rails.
| Layer |
Responsibility |
Rails Component |
| Model |
Represents data and business logic; talks to the database |
ActiveRecord classes in app/models/ |
| View |
Renders what the user sees (usually HTML) |
ERB templates in app/views/ |
| Controller |
Receives requests, talks to models, chooses a view to render |
Classes in app/controllers/ |
A typical request flows like this:
- A browser sends a request to a URL, e.g.
GET /products/3.
- The router matches that URL to a controller action, e.g.
ProductsController#show.
- The controller action asks a model to fetch data (e.g.
Product.find(3)).
- The controller passes that data to a view, which renders HTML.
- The HTML is sent back to the browser.
This request/response cycle, repeated with variations, is the backbone of
almost everything you'll do in Rails.
What This Book Assumes
This book assumes you already know basic Ruby — variables, methods,
classes, blocks, arrays, and hashes. It does not assume you have built
a web application before. By the end, you'll understand enough to build,
test, and deploy a small but real Rails application.
We'll be targeting Rails 8.1, the current stable release line as of
this writing, running on Ruby 3.2 or newer.
2. Setting Up Your Environment
Installing Ruby
Rails 8.1 requires Ruby 3.2.0 or newer. The most reliable way to
install and manage Ruby versions is through a version manager rather than
your operating system's default Ruby.
Recommended: rbenv or mise
# Using rbenv (macOS/Linux)
brew install rbenv
rbenv install 3.3.5
rbenv global 3.3.5
# Verify
ruby -v
# => ruby 3.3.5 ...
If you're on Windows, the simplest path is WSL2 (Windows Subsystem for
Linux) running Ubuntu, and then following the Linux instructions inside
it. Native Windows Ruby installs are possible but cause more friction with
gems that compile native extensions.
Installing a Database
Rails ships with SQLite as the default database for new applications,
including in production for many small-to-medium apps as of Rails 8. This
is a deliberate change from earlier Rails versions, which nudged you
toward PostgreSQL or MySQL for anything beyond local development.
For this book, SQLite is enough — it requires no separate server
process and needs no setup. If you later want PostgreSQL (common for
larger production apps), you can install it and tell Rails to use it when
generating a new app (rails new myapp --database=postgresql).
Installing Node.js (Optional, but Common)
Modern Rails avoids requiring Node.js for many projects thanks to
importmap-rails, which lets you use JavaScript without a build step. If
you plan to use frontend tooling like esbuild, Vite, or npm packages
requiring a bundler, you'll want Node.js installed too. For this book's
examples, it isn't required.
Installing Rails
Once Ruby is installed, installing Rails itself is a single gem install:
gem install rails -v 8.1.3
Verify it worked:
rails -v
# => Rails 8.1.3
A Note on Editors
Any text editor works, but VS Code with the "Ruby LSP" extension (built
and maintained by Shopify) gives you solid autocomplete, go-to-definition,
and inline diagnostics for both Ruby and ERB files. RubyMine is a strong
paid alternative with deeper Rails-specific tooling.
3. Creating Your First Rails App
Generating a New Application
Rails applications start life as a single command:
rails new blog
cd blog
This generates a full directory structure with sensible defaults. Let's
look at what matters most:
blog/
├── app/
│ ├── controllers/ # Request handlers
│ ├── models/ # Data + business logic
│ ├── views/ # Templates
│ ├── helpers/ # View helper methods
│ ├── jobs/ # Background jobs
│ ├── mailers/ # Email
│ └── assets/ # CSS, images
├── config/
│ ├── routes.rb # URL → controller mapping
│ ├── database.yml # Database configuration
│ └── application.rb # App-wide configuration
├── db/
│ ├── migrate/ # Database migration files
│ └── schema.rb # Current database schema (generated)
├── Gemfile # Ruby dependencies
├── Gemfile.lock
└── test/ # Test files
You don't need to memorize this — you'll become familiar with it through
repetition. The important habit to build early is: when in doubt, look
at config/routes.rb first, since it tells you what URLs exist and
which controller/action handles each one.
Starting the Development Server
bin/rails server
# or the shorthand:
bin/rails s
Visit http://localhost:3000 in your browser. You should see the Rails
default welcome page. That confirms your app, database connection, and
server are all working.
The bin/rails Command
Almost everything you do day-to-day in Rails goes through the rails
command-line tool. A few you'll use constantly:
bin/rails generate model Post title:string body:text # scaffolding code
bin/rails db:migrate # apply DB changes
bin/rails console # interactive Ruby + your app loaded
bin/rails routes # list all defined routes
bin/rails test # run the test suite
The Rails console (bin/rails console or bin/rails c) deserves a
special mention: it's an interactive Ruby shell with your entire
application loaded, so you can do things like Post.count or
Post.create!(title: "Hello") directly, without writing a script. You'll
use it constantly for debugging and exploration.
4. Routing
What Routing Does
The router's job is simple: given an incoming HTTP request (a verb like
GET or POST, plus a path like /posts/3), decide which controller
action should handle it. Routes live in config/routes.rb.
Basic Route Syntax
# config/routes.rb
Rails.application.routes.draw do
get "/about", to: "pages#about"
end
This says: "When a GET request comes in for /about, send it to the
about action of PagesController."
Resourceful Routing
Most of the time, you don't write individual routes for each action.
Instead, you declare a resource, and Rails generates a full set of
conventional RESTful routes for you:
Rails.application.routes.draw do
resources :posts
end
This single line generates:
| HTTP Verb |
Path |
Controller#Action |
Purpose |
| GET |
/posts |
posts#index |
List all posts |
| GET |
/posts/new |
posts#new |
Form to create a new post |
| POST |
/posts |
posts#create |
Create a post |
| GET |
/posts/:id |
posts#show |
Show one post |
| GET |
/posts/:id/edit |
posts#edit |
Form to edit a post |
| PATCH/PUT |
/posts/:id |
posts#update |
Update a post |
| DELETE |
/posts/:id |
posts#destroy |
Delete a post |
You can view this same table for your actual app at any time by running:
bin/rails routes
If you only need some of these actions, restrict them:
resources :posts, only: [:index, :show]
Nested Resources
If comments belong to posts, you can nest routes:
resources :posts do
resources :comments, only: [:create, :destroy]
end
This produces paths like /posts/3/comments, reflecting the parent-child
relationship in the URL structure itself.
Root Route
Every app needs a homepage route:
root "posts#index"
5. Controllers and Actions
Anatomy of a Controller
Controllers are plain Ruby classes that inherit from
ApplicationController (which itself inherits from
ActionController::Base). Each public method is called an action, and
corresponds to one of the routes we saw above.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post.all
end
def show
@post = Post.find(params[:id])
end
end
A few things to notice:
- Instance variables (
@posts, @post) set in a controller action are
automatically available in the corresponding view. This is how data
flows from controller to view — no explicit "pass this to the template"
call needed.
params is a hash-like object holding all request parameters — data
from the URL, query string, and submitted forms all end up here.
- If you don't explicitly call
render or redirect_to, Rails
automatically renders the view with the same name as the action (e.g.
index action renders app/views/posts/index.html.erb).
The Full CRUD Controller
Here's a complete controller implementing all seven RESTful actions:
class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :destroy]
def index
@posts = Post.all
end
def show
end
def new
@post = Post.new
end
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: "Post was successfully created."
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @post.update(post_params)
redirect_to @post, notice: "Post was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@post.destroy
redirect_to posts_path, notice: "Post was successfully destroyed."
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :body)
end
end
Two important patterns to internalize here:
before_action runs a method before one or more actions — a clean way
to avoid repeating @post = Post.find(params[:id]) in show, edit,
update, and destroy.
Strong Parameters (post_params) is Rails' mechanism for guarding
against mass-assignment vulnerabilities. params.require(:post) says "I
expect a post key in the submitted params," and .permit(:title, :body)
whitelists exactly which fields are allowed through. Without this, a
malicious user could submit extra form fields (like admin: true) and
have them silently assigned to your model.
6. Models and ActiveRecord
What Is ActiveRecord?
ActiveRecord is Rails' Object-Relational Mapper (ORM). It lets you
interact with database rows as Ruby objects, without writing raw SQL for
most operations. A model class maps to a database table by convention: the
Post class maps to the posts table, Comment maps to comments, and
so on.
Generating a Model
bin/rails generate model Post title:string body:text published:boolean
This creates:
app/models/post.rb — the model class itself
db/migrate/TIMESTAMP_create_posts.rb — a migration describing the
table structure
- a corresponding test file
Migrations
A migration is a versioned, reversible description of a database
schema change, written in Ruby instead of raw SQL:
# db/migrate/20260115000000_create_posts.rb
class CreatePosts < ActiveRecord::Migration[8.1]
def change
create_table :posts do |t|
t.string :title
t.text :body
t.boolean :published, default: false
t.timestamps
end
end
end
t.timestamps is a shorthand that adds created_at and updated_at
columns, which ActiveRecord manages automatically.
Apply pending migrations with:
bin/rails db:migrate
This both updates your actual database and regenerates
db/schema.rb, a single file that represents your database's current
structure — useful for setting up fresh databases (like in CI, or a new
developer's machine) without replaying every migration ever written.
Basic ActiveRecord Queries
Once a model exists, you get a rich query interface for free:
Post.all # every post
Post.find(3) # find by primary key, raises if missing
Post.find_by(title: "Hello World") # find by attribute, returns nil if missing
Post.where(published: true) # filter
Post.where(published: true).order(created_at: :desc) # chainable
Post.count
Post.first
Post.last
post = Post.new(title: "My Post", body: "Content here")
post.save # returns true/false
Post.create(title: "Another Post") # new + save in one call
Post.create!(title: "Another Post") # raises on validation failure
post.update(title: "New Title") # update + save
post.destroy # delete from database
These read almost like English, and that's intentional — it's one of
Rails' most distinctive design choices.
Validations
Validations prevent invalid data from being saved to the database:
class Post < ApplicationRecord
validates :title, presence: true, length: { maximum: 200 }
validates :body, presence: true
end
Now, post.save returns false (rather than raising) if validations
fail, and post.errors gives you details you can display back to the
user:
post = Post.new # no title
post.save # => false
post.errors.full_messages
# => ["Title can't be blank", "Body can't be blank"]
Associations
Real applications have related data. Suppose comments belong to posts:
bin/rails generate model Comment post:references body:text
bin/rails db:migrate
The post:references shorthand creates a post_id foreign key column and
sets up the association automatically:
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
end
# app/models/post.rb
class Post < ApplicationRecord
has_many :comments, dependent: :destroy
end
dependent: :destroy means deleting a post also deletes its comments,
avoiding orphaned rows. With this in place:
post = Post.first
post.comments # all comments for this post
post.comments.create(body: "Nice post!")
comment = Comment.first
comment.post # the parent post
7. Views and ERB Templates
What Views Do
Views are templates that produce the HTML sent back to the browser. Rails'
default templating language is ERB (Embedded Ruby), which lets you mix
Ruby code into HTML using special tags.
ERB Syntax
<%# This is a comment, not rendered %>
<% if @posts.any? %>
<ul>
<% @posts.each do |post| %>
<li><%= post.title %></li>
<% end %>
</ul>
<% else %>
<p>No posts yet.</p>
<% end %>
The rule to remember: <%= %> outputs the result of the Ruby code into
the HTML. <% %> runs Ruby code without outputting anything (used for
loops, conditionals, and control flow).
A Full Example View
<%# app/views/posts/index.html.erb %>
<h1>All Posts</h1>
<%= link_to "New Post", new_post_path, class: "btn" %>
<% @posts.each do |post| %>
<article>
<h2><%= link_to post.title, post %></h2>
<p><%= truncate(post.body, length: 150) %></p>
</article>
<% end %>
Notice link_to post — when you pass a model instance instead of an
explicit path, Rails infers the URL using its resourceful routing
conventions (post_path(post) here). This is a small but characteristic
example of Rails reducing boilerplate through convention.
Layouts and Partials
Layouts wrap your views in a shared HTML skeleton (header, footer,
navigation) so you don't repeat it on every page. By default, every view
renders inside app/views/layouts/application.html.erb:
<!DOCTYPE html>
<html>
<head>
<title>My Blog</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag "application" %>
</head>
<body>
<nav><%= link_to "Home", root_path %></nav>
<%= yield %>
</body>
</html>
<%= yield %> is where the content of the current view gets inserted.
Partials are reusable view fragments, prefixed with an underscore in
their filename:
<%# app/views/posts/_post.html.erb %>
<article>
<h2><%= post.title %></h2>
<p><%= post.body %></p>
</article>
Rendered from another view like this:
<%= render @posts %>
When you pass a collection to render, Rails automatically loops through
it and renders _post.html.erb once per item, inferring the partial name
from the model.
Forms
Rails' form_with helper generates HTML forms bound to a model, including
CSRF protection and correctly-named input fields:
<%# app/views/posts/_form.html.erb %>
<%= form_with model: post do |f| %>
<% if post.errors.any? %>
<div class="errors">
<ul>
<% post.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :title %>
<%= f.text_field :title %>
</div>
<div>
<%= f.label :body %>
<%= f.text_area :body %>
</div>
<%= f.submit %>
<% end %>
This same partial can be reused for both the new and edit views, since
form_with model: post automatically figures out whether it should
POST to create a new record or PATCH to update an existing one, based
on whether post is a new or persisted record.
8. Worked Example: A Task List App
Let's put everything together and build a small but complete task list
app from scratch.
Step 1: Generate the App
rails new tasklist
cd tasklist
Step 2: Generate the Scaffold (or Build by Hand)
Rails offers a scaffold generator that creates a model, controller,
views, and routes all in one command — useful for prototyping, though many
developers prefer building pieces by hand (as we did above) for anything
beyond a quick demo. For this worked example, let's do it by hand so the
pieces are clear.
Model and migration:
bin/rails generate model Task title:string done:boolean
bin/rails db:migrate
# app/models/task.rb
class Task < ApplicationRecord
validates :title, presence: true
end
Routes:
# config/routes.rb
Rails.application.routes.draw do
resources :tasks
root "tasks#index"
end
Controller:
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
before_action :set_task, only: [:edit, :update, :destroy]
def index
@tasks = Task.order(created_at: :desc)
@task = Task.new
end
def create
@task = Task.new(task_params)
if @task.save
redirect_to root_path
else
@tasks = Task.order(created_at: :desc)
render :index, status: :unprocessable_entity
end
end
def update
@task.update(task_params)
redirect_to root_path
end
def destroy
@task.destroy
redirect_to root_path
end
private
def set_task
@task = Task.find(params[:id])
end
def task_params
params.require(:task).permit(:title, :done)
end
end
View:
<%# app/views/tasks/index.html.erb %>
<h1>My Tasks</h1>
<%= form_with model: @task, url: tasks_path do |f| %>
<%= f.text_field :title, placeholder: "New task..." %>
<%= f.submit "Add" %>
<% end %>
<ul>
<% @tasks.each do |task| %>
<li>
<%= form_with model: task, method: :patch do |f| %>
<%= f.check_box :done, onchange: "this.form.requestSubmit()" %>
<% end %>
<span style="<%= 'text-decoration: line-through;' if task.done %>">
<%= task.title %>
</span>
<%= button_to "Delete", task, method: :delete %>
</li>
<% end %>
</ul>
Step 3: Try It
bin/rails server
Visit http://localhost:3000 and you have a working task list: add tasks,
check them off, delete them — with data persisted in SQLite. This tiny app
already demonstrates the full MVC cycle, ActiveRecord validations, and
form handling — the same patterns scale up to much larger applications.
9. Testing Basics
Rails' Built-in Testing (Minitest)
Rails ships with Minitest configured out of the box, no extra setup
needed. Generated models and controllers automatically come with test file
stubs in the test/ directory.
# test/models/task_test.rb
require "test_helper"
class TaskTest < ActiveSupport::TestCase
test "requires a title" do
task = Task.new(title: nil)
assert_not task.valid?
end
test "is valid with a title" do
task = Task.new(title: "Buy milk")
assert task.valid?
end
end
Run the whole suite with:
bin/rails test
System Tests
For testing full user flows through a real (headless) browser, Rails
includes system tests built on Capybara:
# test/system/tasks_test.rb
require "application_system_test_case"
class TasksTest < ApplicationSystemTestCase
test "creating a task" do
visit root_path
fill_in "task_title", with: "Walk the dog"
click_on "Add"
assert_text "Walk the dog"
end
end
Run system tests separately, since they're slower:
bin/rails test:system
Using RSpec Instead
Many teams prefer RSpec over Minitest for its more expressive syntax.
It's not included by default, but adding it is straightforward:
# Gemfile
group :development, :test do
gem "rspec-rails"
end
bundle install
bin/rails generate rspec:install
An equivalent test in RSpec style:
# spec/models/task_spec.rb
require "rails_helper"
RSpec.describe Task, type: :model do
it "requires a title" do
task = Task.new(title: nil)
expect(task).not_to be_valid
end
end
Either framework is a fine choice — Minitest requires zero extra setup,
while RSpec has a larger ecosystem of matchers and community conventions.
Pick one and be consistent within a project.
10. Deployment Basics
Kamal: Rails' Built-in Deployment Tool
Since Rails 8, Kamal (a deployment tool also built by the Rails core
team) is included by default in new applications, aimed at deploying
containerized Rails apps to your own servers or cloud VMs without needing
a full PaaS. It works well if you already have — or are willing to
provision — a Linux server (a $5-6/month VPS is often enough for a small
app).
The essentials:
# config/deploy.yml is generated automatically with `rails new`
# Edit it with your server's IP, domain, and registry details, then:
bin/kamal setup # first-time deployment
bin/kamal deploy # subsequent deployments
Kamal handles building a Docker image, pushing it to a registry, and
restarting the container on your server with zero-downtime deploys.
Platform-as-a-Service Alternatives
If you'd rather not manage a server at all, several platforms support
Rails deployment directly from a Git push:
- Render — straightforward Rails support, free tier available for
small projects, managed PostgreSQL add-ons.
- Fly.io — deploys your app as lightweight VMs close to your users;
Rails apps generated with recent versions include a fly.toml friendly
Dockerfile by default.
- Heroku — the classic choice, though pricing has shifted since it
ended its free tier; still widely documented and well understood.
A typical Render deployment flow:
- Push your code to GitHub.
- Create a new "Web Service" on Render, connect your repo.
- Render detects the Dockerfile (generated by
rails new) and builds it
automatically.
- Set environment variables (like
RAILS_MASTER_KEY, found in
config/master.key) in Render's dashboard.
- Deploy — Render gives you a live URL.
Production Database Considerations
If you used SQLite locally, know that as of Rails 8, SQLite is considered
production-viable for many small-to-medium apps thanks to improvements in
concurrent write handling — a real shift from Rails' historical
"SQLite for development only" guidance. For higher-traffic apps, or apps
needing high concurrency, PostgreSQL remains the more battle-tested
choice, and switching is mostly a matter of updating config/database.yml
and adding the pg gem.
11. Next Steps and Further Resources
You now have a working mental model of Rails: MVC architecture,
resourceful routing, ActiveRecord for data, ERB for views, testing with
Minitest or RSpec, and deployment with Kamal or a PaaS.
Where to go from here:
- Official Rails Guides (guides.rubyonrails.org) — the best
first-party reference, thorough and kept up to date with each release.
- API Documentation (api.rubyonrails.org) — for looking up exact
method signatures once you know roughly what you're looking for.
- Hotwire (Turbo + Stimulus) — Rails' default approach to building
interactive, SPA-like interfaces without writing much JavaScript. Worth
learning right after you're comfortable with the basics in this book.
- ActiveStorage — for handling file uploads (images, documents)
attached to your models.
- ActionMailer — for sending transactional email from your app.
- ActiveJob + Solid Queue — for background job processing (Rails 8
ships with Solid Queue as a database-backed job backend, removing the
need for a separate Redis server in many cases).
- The Rails source code itself — genuinely one of the best-written
large Ruby codebases to read once you're comfortable with the basics;
much of "how Rails works" becomes clear from browsing it.
Rails rewards depth. The conventions that feel arbitrary at first — why
does this file live here, why is this method named that — start to
feel inevitable once you've built a few real applications and internalized
the patterns. The best next step is simple: pick a small idea and build
it, referring back to the Rails Guides whenever you hit something this
book didn't cover.
This book targets Rails 8.1.3, the current stable release as of writing.
Rails moves steadily rather than rapidly — the core concepts here (MVC,
ActiveRecord, resourceful routing) have remained stable for over a decade
and will continue to serve as the right starting point for future
versions.