Ruby
05 / 07

Rails Fundamentals

Ruby: Rails Fundamentals

Ruby on Rails is a full-stack web framework built on the MVC pattern. Convention over configuration: Rails generates predictable paths, names, and structures so you can focus on business logic.

Routes

# config/routes.rb
Rails.application.routes.draw do
  # RESTful resources — generates 7 standard routes (index, show, new, create, edit, update, destroy)
  resources :users
  resources :posts do
    resources :comments, only: [:create, :destroy]  # nested
    member do
      post :publish           # POST /posts/:id/publish
    end
    collection do
      get :archived           # GET /posts/archived
    end
  end

  # Custom routes
  get  '/login',  to: 'sessions#new'
  post '/login',  to: 'sessions#create'
  delete '/logout', to: 'sessions#destroy'

  # Namespace (admin panel)
  namespace :admin do
    resources :users          # /admin/users → Admin::UsersController
  end

  root 'home#index'           # GET / → HomeController#index
end

# Inspect routes
# rails routes
# rails routes --grep users

Controllers

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  before_action :authorize_owner!, only: [:edit, :update, :destroy]

  def index
    @posts = Post.published.order(created_at: :desc).page(params[:page])
    render json: @posts   # or just let it render index.html.erb
  end

  def show
    # @post set by before_action
  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

  def update
    if @post.update(post_params)
      redirect_to @post
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @post.destroy
    redirect_to posts_path, status: :see_other
  end

  private

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

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

  def authorize_owner!
    redirect_to root_path unless @post.user == current_user
  end
end

ActiveRecord: Models & Associations

# app/models/user.rb
class User < ApplicationRecord
  # Associations
  has_many :posts, dependent: :destroy
  has_many :comments, through: :posts
  has_one :profile, dependent: :destroy
  belongs_to :organization, optional: true
  has_and_belongs_to_many :roles

  # Validations
  validates :email, presence: true, uniqueness: { case_sensitive: false },
                    format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :name, presence: true, length: { minimum: 2, maximum: 100 }
  validates :age, numericality: { greater_than: 0, allow_nil: true }

  # Callbacks
  before_save :normalize_email
  after_create :send_welcome_email

  # Scopes
  scope :active,    -> { where(active: true) }
  scope :admins,    -> { where(role: 'admin') }
  scope :recent,    -> { order(created_at: :desc) }

  # Enum
  enum status: { pending: 0, active: 1, suspended: 2 }

  private

  def normalize_email
    self.email = email.downcase.strip
  end
end

# Queries
User.all
User.find(1)
User.find_by(email: 'alice@example.com')   # nil if not found
User.find_by!(email: '...')               # raises if not found
User.where(active: true).order(:name).limit(10)
User.where('age > ?', 18)
User.where(age: 18..65)
User.joins(:posts).where(posts: { published: true })
User.includes(:posts, :profile)            # eager load — avoid N+1
User.count
User.pluck(:email)                         # ["alice@...", "bob@..."] — no model objects

Migrations & Database

# Generate migration
# rails generate migration AddPublishedToPosts published:boolean:index

class AddPublishedToPosts < ActiveRecord::Migration[7.2]
  def change
    add_column :posts, :published, :boolean, default: false, null: false
    add_index  :posts, :published
    add_column :posts, :published_at, :datetime

    # Other common helpers:
    # add_reference :posts, :category, null: false, foreign_key: true
    # rename_column :users, :username, :login
    # remove_column :users, :legacy_field
    # change_column_null :users, :email, false
  end
end

# Run migrations
# rails db:migrate
# rails db:migrate:status
# rails db:rollback STEP=2
# rails db:schema:load   # recreate from schema.rb (faster for fresh setup)

# Seeds (db/seeds.rb)
User.create!(name: 'Admin', email: 'admin@example.com', role: 'admin')
10.times { User.create!(Faker::User.safe_message) }

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free