Ruby Framework

Ruby on Rails

Rails is the full-stack Ruby framework built on convention over configuration. It decides the boring parts — file names, table names, routes — so you can spend your time on the product.

intermediate15 min readUpdated Sep 16, 2026
app/controllers/posts_controller.rb
ruby
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: %i[show update destroy]

  def index
    @posts = Post.includes(:author).published.recent
  end

  def create
    @post = Current.user.posts.build(post_params)
    if @post.save
      redirect_to @post, notice: "Post created"
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end

  def post_params
    params.require(:post).permit(:title, :body)
  end
end
Released
2004
Created by
David Heinemeier Hansson
Language
Ruby
Style
Convention over configuration
Data layer
Active Record ORM
Version
8.x

Why it matters

Why Rails is so fast to build with

CRUD in an afternoon

Generators, migrations and scaffolding turn a data model into a working, database-backed application in hours rather than weeks.

Convention over configuration

Sensible defaults decide file names, table names and routes. You override them only when your domain actually needs something different.

Gems for everything

Authentication, payments, file uploads, search and admin dashboards all have mature, battle-tested gems maintained by the community.

The big picture

MVC, conventions, gems

Rails gives every request the same shape: a route, a controller, a model and a view, named the way the framework already expects.

Convention over configuration

Decide

The framework makes the default choice so you never have to. A Post model maps to a posts table and a /posts route without configuration.

Active Record

Persist

Models wrap database tables with associations, validations, scopes and migrations, keeping SQL out of your everyday code.

RESTful resources

Expose

resources :posts declares the seven conventional CRUD routes, and controllers follow the same actions for every resource.

At a glance

The Rails toolbox

RESTful routes

resources :posts gives you index, show, new, create, edit, update and destroy.

Active Record

Models, associations and migrations describe your schema in Ruby.

MVC

Controllers coordinate, models hold the domain, views render the response.

Generators

rails generate writes models, controllers, migrations and tests for you.

Hotwire

Turbo and Stimulus add interactivity without a separate SPA.

Background jobs

Active Job plus Sidekiq runs slow work outside the request cycle.

A short history

Two decades of the Rails way

  1. 2004

    Rails is extracted from Basecamp

    David Heinemeier Hansson pulls a framework out of the Basecamp codebase and releases it as open source.

    04
  2. 2005

    Rails 1.0

    The first stable release makes convention over configuration a mainstream idea in web development.

    05
  3. 2008

    Rails 2

    REST becomes the default routing style and resources shape every controller.

    08
  4. 2010

    Rails 3 and Bundler

    A cleaner API, Bundler for dependencies and the merger with Merb modernise the stack.

    10
  5. 2016

    Rails 5 and Action Cable

    WebSockets arrive in the core, and the API-only mode targets single-page front ends.

    16
  6. 2021

    Rails 7 and Hotwire

    Turbo and Stimulus replace Turbolinks, and import maps ship JavaScript without a bundler.

    21
  7. 2024

    Rails 8

    Solid Queue, Solid Cache and Solid Cable run on the database, and Kamal simplifies deployment.

    24

The complete guide

Ruby on Rails: Everything you need to know

What is Ruby on Rails?

Rails is a full-stack web framework for Ruby. It ships with everything a database-backed web application needs: an ORM, a router, a view layer, background jobs, mailers, caching, WebSockets and a test harness. Instead of choosing and wiring those pieces yourself, you inherit a coherent stack that was designed to work together.

Its defining idea is convention over configuration. The framework assumes a sensible structure — Post lives in app/models/post.rb, maps to a posts table, and is served by PostsController — and you only write configuration when you deviate from it. The Rails team calls this philosophy omakase, the chef’s choice: trust the defaults, and you will move faster.

That trade is why Rails has powered products from Basecamp and GitHub to Shopify and HEY for twenty years, and why a single developer can still ship a serious application in a weekend.

Convention over configuration

Every Rails project has the same skeleton, so you never wonder where something goes.

app/
├── controllers/    # request handling
├── models/         # domain and persistence
├── views/          # templates
├── jobs/           # background work
├── mailers/        # email
└── javascript/     # Stimulus controllers
config/
├── routes.rb       # the URL map
└── database.yml    # connection settings
db/
└── migrate/        # schema history

The payoff is predictability. A new developer opens the repository and already knows that the User model is in app/models/user.rb and that its table is users. Naming is not a style debate; it is an interface the framework relies on.

The MVC request cycle

Rails is a Model-View-Controller framework, and every HTTP request follows the same path:

  1. Routing matches the URL to a controller action.
  2. The controller loads or changes data through models.
  3. Models enforce the domain and talk to the database.
  4. A view renders the response, usually as HTML.
# config/routes.rb
Rails.application.routes.draw do
  resources :posts
  root "posts#index"
end

A request to GET /posts lands on PostsController#index, which prepares @posts, after which app/views/posts/index.html.erb is rendered. Keeping each layer focused is what keeps a Rails app maintainable as it grows.

The command line is the framework

You barely touch Rails through configuration files. Most of the work happens in the terminal.

rails new shop --database=postgresql
cd shop

bin/rails generate scaffold Post title:string body:text published:boolean
bin/rails generate model Comment post:references body:text
bin/rails generate controller Pages home about

bin/rails db:create db:migrate
bin/rails server

generate writes the model, migration, controller, routes, views and tests for you, and the generator output tells you what it changed. Read every generated file before you keep it — the fastest way to learn Rails is to study what it writes and then delete what you do not need.

Active Record: the model layer

Active Record is the ORM. A model class wraps a database table, and an instance wraps a row.

Migrations

Migrations are versioned, reversible schema changes stored in db/migrate. They are the only supported way to change the database.

class CreatePosts < ActiveRecord::Migration[8.0]
  def change
    create_table :posts do |t|
      t.string :title, null: false
      t.text :body, null: false
      t.boolean :published, default: false, null: false
      t.references :author, null: false, foreign_key: { to_table: :users }
      t.timestamps
    end

    add_index :posts, %i[published created_at]
  end
end

Run bin/rails db:migrate to apply it and bin/rails db:rollback to undo it. Because change is reversible, Rails knows how to reverse the operations without you writing a down method.

Associations

Associations describe relationships and generate convenient methods.

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :comments, through: :posts
end

class Post < ApplicationRecord
  belongs_to :author, class_name: "User", inverse_of: :posts
  has_many :comments, dependent: :destroy
  has_one_attached :cover
end

With that in place, user.posts, post.author and post.comments.create(body: "...") all work, and dependent: :destroy keeps orphaned rows out of the database.

Validations and scopes

Validations protect the domain at the model layer, where every write passes through.

class Post < ApplicationRecord
  validates :title, presence: true, length: { maximum: 120 }
  validates :body, presence: true
  validates :slug, uniqueness: true

  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc) }

  before_validation :generate_slug, on: :create

  private

  def generate_slug
    self.slug ||= title.to_s.parameterize
  end
end

Scopes return relations, so they compose: Post.published.recent.limit(20) reads like a sentence and runs as one query. When a scope grows past a line or two, promote it to a class method so it stays readable.

Avoiding N+1 queries

The most common performance problem in Rails is the N+1 query: one query for a list, then one more for each row’s association.

# Two queries total, no matter how many posts.
posts = Post.includes(:author, :comments).recent

posts.each do |post|
  puts "#{post.title} by #{post.author.name}"
end

Use includes when you will access the association, preload when you want a separate query, and eager_load when you also filter on the association. Add the bullet gem to development and it will flag every N+1 before it reaches production.

Controllers and RESTful routes

resources declares the conventional CRUD routes and maps them to controller actions with matching names.

# config/routes.rb
Rails.application.routes.draw do
  resources :posts do
    resources :comments, only: %i[index create destroy]
  end

  namespace :api do
    namespace :v1 do
      resources :posts, only: %i[index show create update destroy]
    end
  end
end

Controllers stay thin. They load data, call the domain, and choose a response. Business logic belongs in models or service objects, not in actions.

class PostsController < ApplicationController
  before_action :set_post, only: %i[show update destroy]

  def index
    @posts = Post.published.includes(:author).recent
  end

  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to @post, notice: "Post created."
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end

  def post_params
    params.require(:post).permit(:title, :body, :published, tag_ids: [])
  end
end

Strong parameters are not optional. params.require(:post).permit(...) is the boundary that stops a client from setting columns you never intended, such as admin or role.

Views, ERB and layouts

Views are ERB templates: HTML with embedded Ruby. Rails escapes output by default, which removes a whole class of cross-site scripting bugs.

<%# app/views/posts/index.html.erb %>
<h1>Posts</h1>

<%= link_to "New post", new_post_path %>

<ul>
  <% @posts.each do |post| %>
    <li>
      <%= link_to post.title, post %>
      <span><%= pluralize(post.comments.size, "comment") %></span>
    </li>
  <% end %>
</ul>

Use <%= %> to output and <% %> to execute without output. A layout in app/views/layouts/application.html.erb wraps every page, and partials — files beginning with an underscore — let you reuse markup with render "post", post: post or simply render @posts.

Hotwire: Turbo and Stimulus

Modern Rails adds interactivity without a single-page app. Turbo intercepts links and forms and swaps page fragments over the wire; Stimulus adds small JavaScript behaviours where you genuinely need them.

<%# Turbo Frames scope an update to one part of the page %>
<%= turbo_frame_tag "comments" do %>
  <%= render @post.comments %>
<% end %>
// app/javascript/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["menu"];

  toggle() {
    this.menuTarget.classList.toggle("hidden");
  }
}

This is progressive enhancement in practice: server-rendered HTML is the baseline, and JavaScript upgrades it. Most Rails apps never need a client-side router.

Assets and import maps

Rails 7 replaced the old Sprockets pipeline with import maps by default and offers Propshaft as a simpler asset pipeline. You can pin a library without a Node build step.

bin/importmap pin local-time
bin/rails assets:precompile

If you prefer a bundler, bin/rails javascript:install:esbuild or :vite switches the setup. The principle is the same: serve what the browser understands and keep the toolchain out of the way until you need it.

Background jobs

Anything slow — sending email, calling an API, generating a report — belongs in a job, not in the request. Active Job is the common interface; the adapter decides where jobs run.

class PublishPostJob < ApplicationJob
  queue_as :default

  retry_on Net::ReadTimeout, wait: :polynomially_longer, attempts: 5

  def perform(post_id)
    Post.find(post_id).publish!
  end
end

PublishPostJob.perform_later(post.id)
# config/application.rb
config.active_job.queue_adapter = :solid_queue # or :sidekiq

Rails 8 ships Solid Queue, which keeps jobs in the database and needs no Redis, so a single-server app has one fewer moving part. Sidekiq remains a strong choice at high volume.

Testing

Rails generates tests with everything it creates. Minitest ships in the box and is fast; RSpec is the popular alternative with a more expressive syntax.

# test/models/post_test.rb
require "test_helper"

class PostTest < ActiveSupport::TestCase
  test "requires a title" do
    post = Post.new(body: "Hello")
    assert_not post.valid?
    assert_includes post.errors[:title], "can't be blank"
  end
end
# spec/models/post_spec.rb
RSpec.describe Post, type: :model do
  it "is invalid without a title" do
    expect(Post.new(body: "Hello")).not_to be_valid
  end
end

Controller tests make real requests in-process, which catches routing and parameter bugs that unit tests miss.

# test/controllers/posts_controller_test.rb
class PostsControllerTest < ActionDispatch::IntegrationTest
  test "lists published posts" do
    Post.create!(title: "Hi", body: "There", published: true)
    get posts_url
    assert_response :success
  end
end

Run everything with bin/rails test or bundle exec rspec. Add bin/rails test:system for browser-driven system tests when a flow matters enough to test end to end.

The Rails console

bin/rails console boots your application with every model and helper available. It is the fastest way to explore data, test a query and reproduce a bug.

bin/rails console
bin/rails console --sandbox   # rolls back all changes on exit
Post.published.recent.limit(5)
Post.where(published: false).update_all(published: true)
Post.includes(:author).find_each { |post| puts post.title }

--sandbox is a safety net while you experiment: any write is rolled back when you quit.

Best practices

  • Keep controllers thin; move domain logic into models or service objects.
  • Eager load associations with includes and watch the logs for N+1 queries.
  • Change the database only through migrations, and commit them with the code.
  • Use strong parameters for every write and validate at the model layer.
  • Add database indexes for the columns you filter and sort on.
  • Push slow work into background jobs instead of the request cycle.
  • Write a test for every bug you fix, then keep the suite fast.

Common mistakes

  • Rendering a list without includes and shipping an N+1 to production.
  • Putting business logic in controllers until actions are hundreds of lines long.
  • Using params[:post] directly instead of strong parameters.
  • Editing db/schema.rb by hand instead of writing a migration.
  • Forgetting dependent: options and leaving orphaned records behind.
  • Reaching for a service object or a gem before the model has earned it.
  • Running heavy queries in views instead of loading data in the controller.

Where to go next

Rails teaches a durable way of thinking: resources, conventions and a single place for each concern. If you like the batteries-included model, Laravel applies the same philosophy in PHP, while Django does it in Python. Because Rails routing is built on resources, the REST APIs guide explains the ideas underneath. And since most Rails apps run on a relational database, deepen your PostgreSQL knowledge next.

In practice

The four files behind one resource

Switch between the route, the controller, the model and the migration to see how Rails wires a resource together.

config/routes.rb
Rails.application.routes.draw do
  resources :posts do
    resources :comments, only: %i[index create destroy]
    member do
      post :publish
    end
  end

  namespace :api do
    namespace :v1 do
      resources :posts, only: %i[index show create update destroy]
    end
  end

  root "posts#index"
end

Loading associations

Accessing an association inside a loop issues one query per row. Eager load it once and the whole page runs two queries.

Prefer
posts = Post.includes(:author, :comments).recent
posts.each { |post| puts post.author.name }
Avoid
posts = Post.recent
posts.each { |post| puts post.author.name }
# one query per post: the classic N+1

Accepting input

Strong parameters whitelist what a request may set. Passing the raw hash lets a client assign any column, including admin flags.

Prefer
def post_params
  params.require(:post).permit(:title, :body, tag_ids: [])
end
Avoid
Post.new(params[:post])
# mass assignment: role and admin are fair game

Trade-offs

Is Rails the right default?

Rails trades some freedom for speed of delivery. That is a great deal until your app outgrows the conventions.

Strengths

  • You ship features, not plumbing

    Routing, migrations, validation, mailers, jobs and caching are all included and already wired together, so a small team can build a large product.

  • The conventions are a shared language

    Any Rails developer can open any Rails app and know where things live. That makes hiring and onboarding dramatically easier.

  • The gem ecosystem is deep

    Devise, Pundit, Sidekiq, RSpec and countless others solve the problems every application eventually faces.

Trade-offs

  • The magic hides the framework

    Convention feels effortless until something is not conventional. Debugging requires understanding the underlying Rails source, not just your code.

  • Performance needs attention

    A naive Rails app is not slow, but N+1 queries, missing indexes and heavy views add up. Profiling is a required skill, not an optional one.

  • The full stack is opinionated

    Rails assumes HTML, a relational database and its own asset story. If your architecture diverges, you fight the defaults more than you use them.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Rails?

Our interactive tutorial walks you through Rails step by step — with quizzes and real code you can run in the browser.