Ruby on Rails Complete Guide

Table of Contents

  1. Introduction
  2. Installation and Setup
  3. Rails Architecture (MVC)
  4. Getting Started
  5. Models and ActiveRecord
  6. Views and ERB Templates
  7. Controllers
  8. Routing
  9. Database Migrations
  10. Associations
  11. Validations
  12. Forms and Form Helpers
  13. Authentication and Authorization
  14. Testing
  15. Deployment
  16. Best Practices
  17. Advanced Topics

Introduction

Ruby on Rails (often just called "Rails") is a web application framework written in Ruby. It follows the Model-View-Controller (MVC) architectural pattern and emphasizes convention over configuration (CoC) and don't repeat yourself (DRY) principles.

Key Features

  • Convention over Configuration: Rails makes assumptions about what you want to do and how you're going to do it
  • DRY (Don't Repeat Yourself): Every piece of knowledge must have a single, unambiguous representation
  • RESTful Design: Rails encourages RESTful application design
  • Active Record Pattern: Object-relational mapping (ORM) framework
  • Built-in Testing: Comprehensive testing framework included

Installation and Setup

Prerequisites

  • Ruby (version 2.7.0 or higher)
  • Node.js (for JavaScript runtime)
  • Yarn or npm (for managing JavaScript packages)
  • SQLite3, PostgreSQL, or MySQL (database)

Installing Rails

# Install Rails gem
gem install rails

# Verify installation
rails --version

# Create a new Rails application
rails new myapp

# Navigate to the application directory
cd myapp

# Start the Rails server
rails server
# or
rails s

Project Structure

myapp/
├── app/
│   ├── controllers/
│   ├── models/
│   ├── views/
│   ├── helpers/
│   ├── mailers/
│   └── assets/
├── config/
├── db/
├── lib/
├── public/
├── test/
├── vendor/
├── Gemfile
└── Rakefile

Rails Architecture (MVC)

Rails follows the Model-View-Controller pattern:

Model

  • Represents data and business logic
  • Handles database interactions through ActiveRecord
  • Contains validations and associations

View

  • Presentation layer
  • ERB templates that generate HTML
  • Handles user interface

Controller

  • Handles user input and coordinates between Model and View
  • Processes HTTP requests
  • Renders appropriate responses

Getting Started

Creating Your First Controller

# Generate a controller
rails generate controller Welcome index

This creates:

  • app/controllers/welcome_controller.rb
  • app/views/welcome/index.html.erb
  • Routes entry

Basic Controller Example

# app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController
  def index
    @message = "Hello, Rails!"
  end
end

Basic View Example

<!-- app/views/welcome/index.html.erb -->
<h1>Welcome to Rails</h1>
<p><%= @message %></p>

Setting Root Route

# config/routes.rb
Rails.application.routes.draw do
  root 'welcome#index'
end

Models and ActiveRecord

Generating a Model

# Generate a model with attributes
rails generate model Article title:string body:text published:boolean

# Run the migration
rails db:migrate

Basic Model Example

# app/models/article.rb
class Article < ApplicationRecord
  validates :title, presence: true, length: { minimum: 5 }
  validates :body, presence: true
  
  scope :published, -> { where(published: true) }
  
  def summary
    body.truncate(100)
  end
end

ActiveRecord Basics

# Creating records
article = Article.new(title: "My Title", body: "Content")
article.save

# or
article = Article.create(title: "My Title", body: "Content")

# Finding records
Article.all
Article.find(1)
Article.find_by(title: "My Title")
Article.where(published: true)

# Updating records
article.update(title: "New Title")

# Deleting records
article.destroy

Views and ERB Templates

ERB Syntax

<!-- Output Ruby code -->
<%= ruby_code %>

<!-- Execute Ruby code without output -->
<% ruby_code %>

<!-- Comments -->
<%# This is a comment %>

Layout Example

<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <header>
      <nav>
        <%= link_to "Home", root_path %>
        <%= link_to "Articles", articles_path %>
      </nav>
    </header>
    
    <main>
      <%= yield %>
    </main>
  </body>
</html>

Partials

<!-- app/views/shared/_header.html.erb -->
<header>
  <h1>My Application</h1>
</header>

<!-- Using the partial -->
<%= render 'shared/header' %>

<!-- Partial with local variables -->
<%= render 'article', article: @article %>

Controllers

RESTful Controller Example

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :edit, :update, :destroy]
  
  def index
    @articles = Article.all
  end
  
  def show
  end
  
  def new
    @article = Article.new
  end
  
  def create
    @article = Article.new(article_params)
    
    if @article.save
      redirect_to @article, notice: 'Article was successfully created.'
    else
      render :new
    end
  end
  
  def edit
  end
  
  def update
    if @article.update(article_params)
      redirect_to @article, notice: 'Article was successfully updated.'
    else
      render :edit
    end
  end
  
  def destroy
    @article.destroy
    redirect_to articles_url, notice: 'Article was successfully deleted.'
  end
  
  private
  
  def set_article
    @article = Article.find(params[:id])
  end
  
  def article_params
    params.require(:article).permit(:title, :body, :published)
  end
end

Controller Filters

class ApplicationController < ActionController::Base
  before_action :authenticate_user!
  before_action :set_current_user
  after_action :log_action
  around_action :catch_exceptions
  
  private
  
  def set_current_user
    @current_user = User.find(session[:user_id]) if session[:user_id]
  end
  
  def log_action
    Rails.logger.info "Action completed: #{action_name}"
  end
  
  def catch_exceptions
    begin
      yield
    rescue => e
      Rails.logger.error "Error: #{e.message}"
      redirect_to root_path, alert: "Something went wrong"
    end
  end
end

Routing

Basic Routes

# config/routes.rb
Rails.application.routes.draw do
  root 'welcome#index'
  
  # RESTful routes
  resources :articles
  
  # Custom routes
  get 'about', to: 'pages#about'
  post 'contact', to: 'pages#contact'
  
  # Nested routes
  resources :articles do
    resources :comments
  end
  
  # Namespace
  namespace :admin do
    resources :users
  end
  
  # Route constraints
  get 'articles/:id', to: 'articles#show', constraints: { id: /\d+/ }
end

Route Helpers

# In controllers and views
articles_path          # /articles
article_path(@article) # /articles/1
new_article_path       # /articles/new
edit_article_path(@article) # /articles/1/edit

# With parameters
article_path(@article, format: :json)
articles_path(page: 2)

Database Migrations

Creating Migrations

# Generate migration
rails generate migration CreateArticles title:string body:text published:boolean

# Add column
rails generate migration AddAuthorToArticles author:string

# Remove column
rails generate migration RemoveAuthorFromArticles author:string

# Create join table
rails generate migration CreateJoinTableArticlesTags articles tags

Migration Example

# db/migrate/20231201000000_create_articles.rb
class CreateArticles < ActiveRecord::Migration[7.0]
  def change
    create_table :articles do |t|
      t.string :title, null: false
      t.text :body
      t.boolean :published, default: false
      t.references :user, null: false, foreign_key: true
      t.timestamps
    end
    
    add_index :articles, :published
    add_index :articles, [:user_id, :created_at]
  end
end

Running Migrations

# Run pending migrations
rails db:migrate

# Rollback last migration
rails db:rollback

# Rollback specific number of migrations
rails db:rollback STEP=3

# Reset database
rails db:reset

# Drop and recreate database
rails db:drop db:create db:migrate

Associations

Types of Associations

# One-to-many
class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end

class Article < ApplicationRecord
  belongs_to :user
end

# Many-to-many
class Article < ApplicationRecord
  has_many :article_tags
  has_many :tags, through: :article_tags
end

class Tag < ApplicationRecord
  has_many :article_tags
  has_many :articles, through: :article_tags
end

class ArticleTag < ApplicationRecord
  belongs_to :article
  belongs_to :tag
end

# One-to-one
class User < ApplicationRecord
  has_one :profile, dependent: :destroy
end

class Profile < ApplicationRecord
  belongs_to :user
end

Association Options

class User < ApplicationRecord
  has_many :articles, 
           dependent: :destroy,
           foreign_key: 'author_id',
           class_name: 'Article',
           inverse_of: :author
           
  has_many :published_articles, 
           -> { where(published: true) },
           class_name: 'Article'
end

Validations

Common Validations

class Article < ApplicationRecord
  validates :title, presence: true,
                   length: { minimum: 5, maximum: 100 },
                   uniqueness: true
                   
  validates :body, presence: true,
                  length: { minimum: 10 }
                  
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
  
  validates :age, numericality: { greater_than: 0, less_than: 150 }
  
  validates :terms_of_service, acceptance: true
  
  validates :password, confirmation: true
  
  validate :custom_validation
  
  private
  
  def custom_validation
    if title.present? && title.include?('spam')
      errors.add(:title, 'cannot contain spam')
    end
  end
end

Conditional Validations

class Article < ApplicationRecord
  validates :body, presence: true, if: :published?
  validates :summary, presence: true, unless: :draft?
  
  validates :title, presence: true, if: -> { published && created_at > 1.week.ago }
end

Forms and Form Helpers

Form Helpers

<!-- app/views/articles/_form.html.erb -->
<%= form_with model: @article, local: true do |form| %>
  <% if @article.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>
      <ul>
        <% @article.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>

  <div class="field">
    <%= form.label :body %>
    <%= form.text_area :body %>
  </div>

  <div class="field">
    <%= form.check_box :published %>
    <%= form.label :published %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

Form Types

<!-- Text inputs -->
<%= form.text_field :title %>
<%= form.text_area :body %>
<%= form.password_field :password %>
<%= form.email_field :email %>
<%= form.number_field :age %>

<!-- Select inputs -->
<%= form.select :category, ['Tech', 'Sports', 'News'] %>
<%= form.collection_select :user_id, User.all, :id, :name %>

<!-- Checkboxes and radio buttons -->
<%= form.check_box :published %>
<%= form.radio_button :status, 'active' %>

<!-- File uploads -->
<%= form.file_field :image %>

<!-- Hidden fields -->
<%= form.hidden_field :user_id %>

Authentication and Authorization

# Gemfile
gem 'devise'

# Install Devise
rails generate devise:install
rails generate devise User
rails db:migrate

Basic Authentication

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :authenticate_user!
  before_action :configure_permitted_parameters, if: :devise_controller?
  
  protected
  
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name])
    devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name])
  end
end

Authorization with CanCanCan

# Gemfile
gem 'cancancan'

# app/models/ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)
    
    if user.admin?
      can :manage, :all
    else
      can :read, Article, published: true
      can :manage, Article, user_id: user.id
    end
  end
end

# In controllers
class ArticlesController < ApplicationController
  load_and_authorize_resource
  
  def index
    # @articles is automatically loaded and authorized
  end
end

Testing

Test Types in Rails

Unit Tests (Models)

# test/models/article_test.rb
require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  test "should not save article without title" do
    article = Article.new
    assert_not article.save
  end
  
  test "should save article with valid attributes" do
    article = Article.new(title: "Test Title", body: "Test body")
    assert article.save
  end
  
  test "title should be at least 5 characters" do
    article = Article.new(title: "Hi", body: "Test body")
    assert_not article.valid?
    assert_includes article.errors[:title], "is too short (minimum is 5 characters)"
  end
end

Integration Tests (Controllers)

# test/controllers/articles_controller_test.rb
require 'test_helper'

class ArticlesControllerTest < ActionDispatch::IntegrationTest
  setup do
    @article = articles(:one)
  end

  test "should get index" do
    get articles_url
    assert_response :success
  end

  test "should create article" do
    assert_difference('Article.count') do
      post articles_url, params: { article: { title: "New Article", body: "Article body" } }
    end

    assert_redirected_to article_url(Article.last)
  end
end

System Tests (Full Stack)

# test/system/articles_test.rb
require "application_system_test_case"

class ArticlesTest < ApplicationSystemTestCase
  test "creating an article" do
    visit articles_path
    click_on "New Article"
    
    fill_in "Title", with: "Test Article"
    fill_in "Body", with: "This is a test article"
    click_on "Create Article"
    
    assert_text "Article was successfully created"
  end
end

Test Fixtures

# test/fixtures/articles.yml
one:
  title: "First Article"
  body: "This is the first article"
  published: true

two:
  title: "Second Article"
  body: "This is the second article"
  published: false

Running Tests

# Run all tests
rails test

# Run specific test file
rails test test/models/article_test.rb

# Run specific test
rails test test/models/article_test.rb:test_should_not_save_article_without_title

# Run tests with coverage
rails test --coverage

Deployment

Preparing for Production

# config/environments/production.rb
Rails.application.configure do
  config.cache_classes = true
  config.eager_load = true
  config.consider_all_requests_local = false
  config.action_controller.perform_caching = true
  config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
  config.assets.compile = false
  config.active_storage.variant_processor = :mini_magick
  config.log_level = :info
  config.force_ssl = true
end

Environment Variables

# config/database.yml
production:
  <<: *default
  database: <%= ENV['DATABASE_URL'] %>

# In your app
class ApplicationController < ActionController::Base
  before_action :set_api_key
  
  private
  
  def set_api_key
    @api_key = ENV['API_KEY']
  end
end

Deployment to Heroku

# Install Heroku CLI and login
heroku login

# Create Heroku app
heroku create myapp

# Add PostgreSQL addon
heroku addons:create heroku-postgresql:hobby-dev

# Deploy
git push heroku main

# Run migrations
heroku run rails db:migrate

# Open app
heroku open

Deployment with Docker

# Dockerfile
FROM ruby:3.0

WORKDIR /app

COPY Gemfile Gemfile.lock ./
RUN bundle install

COPY . .

EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]

Best Practices

Code Organization

  1. Fat Models, Skinny Controllers: Keep business logic in models
  2. Use Service Objects: For complex business logic
  3. Follow RESTful conventions: Use standard CRUD operations
  4. Use Concerns: For shared functionality
# app/models/concerns/publishable.rb
module Publishable
  extend ActiveSupport::Concern
  
  included do
    scope :published, -> { where(published: true) }
    scope :draft, -> { where(published: false) }
  end
  
  def publish!
    update!(published: true, published_at: Time.current)
  end
end

# app/models/article.rb
class Article < ApplicationRecord
  include Publishable
end

Security Best Practices

  1. Use Strong Parameters: Always filter parameters
  2. Protect from CSRF: Use protect_from_forgery
  3. Validate User Input: Use model validations
  4. Use HTTPS: Force SSL in production
  5. Keep Dependencies Updated: Regular bundle update

Performance Optimization

# Use includes to avoid N+1 queries
@articles = Article.includes(:user, :comments).all

# Use select to limit columns
@articles = Article.select(:id, :title, :created_at).all

# Use counter caches
class Article < ApplicationRecord
  belongs_to :user, counter_cache: true
end

# Add database indexes
add_index :articles, :user_id
add_index :articles, [:published, :created_at]

Advanced Topics

Background Jobs with Active Job

# app/jobs/article_notification_job.rb
class ArticleNotificationJob < ApplicationJob
  queue_as :default

  def perform(article)
    ArticleMailer.notification(article).deliver_now
  end
end

# Usage
ArticleNotificationJob.perform_later(@article)

Caching

# Fragment caching
<% cache @article do %>
  <%= render @article %>
<% end %>

# Low-level caching
Rails.cache.fetch("expensive_operation", expires_in: 1.hour) do
  expensive_operation
end

# Russian Doll caching
<% cache [@article, @article.comments.maximum(:updated_at)] do %>
  <%= render @article %>
  <%= render @article.comments %>
<% end %>

API Development

# app/controllers/api/v1/articles_controller.rb
class Api::V1::ArticlesController < ApplicationController
  respond_to :json
  
  def index
    @articles = Article.all
    render json: @articles
  end
  
  def show
    @article = Article.find(params[:id])
    render json: @article, include: [:user, :comments]
  end
  
  def create
    @article = Article.new(article_params)
    
    if @article.save
      render json: @article, status: :created
    else
      render json: { errors: @article.errors }, status: :unprocessable_entity
    end
  end
end

WebSockets with Action Cable

# app/channels/comments_channel.rb
class CommentsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "comments_#{params[:article_id]}"
  end
  
  def unsubscribed
    # Cleanup
  end
end

# Broadcasting
ActionCable.server.broadcast("comments_#{@article.id}", {
  comment: render_comment(@comment)
})