Toolmingo
Guides13 min read

Ruby on Rails Cheat Sheet: Commands, Routing, Models & Patterns

A complete Ruby on Rails cheat sheet — Rails CLI, routing, Active Record, controllers, views, migrations, associations, validations, and authentication patterns with copy-ready examples.

Ruby on Rails is a full-stack web framework built on Ruby that follows Convention over Configuration and Don't Repeat Yourself (DRY). This cheat sheet covers the Rails CLI, routing, Active Record, controllers, views, migrations, and real-world patterns — with copy-ready examples.

Quick reference

Task Command / Code
New app rails new myapp --database=postgresql
Start server rails server (or rails s)
Rails console rails console (or rails c)
Generate scaffold rails generate scaffold Post title:string body:text
Run migrations rails db:migrate
Rollback migration rails db:rollback
List routes rails routes
Run tests rails test
Find record Post.find(1)
Find by field Post.find_by(title: "Hello")
All records Post.all
Create record Post.create!(title: "Hello")
Update record post.update(title: "New")
Delete record post.destroy
Scope Post.where(published: true).order(:created_at)
Associations has_many :comments, belongs_to :user
Before action before_action :authenticate_user!
Render JSON render json: @post
Redirect redirect_to posts_path
Flash notice flash[:notice] = "Saved"
Partial <%= render "shared/header" %>
Link helper <%= link_to "Show", post_path(@post) %>
Form helper `<%= form_with model: @post do

Rails CLI

New application

# New app with default SQLite
rails new myapp

# PostgreSQL database
rails new myapp --database=postgresql

# API-only mode (no views)
rails new myapp --api

# Skip test framework (for RSpec)
rails new myapp --skip-test

# Combine options
rails new myapp --database=postgresql --skip-test --css=tailwind

Generate commands

# Scaffold (model + controller + views + migration + routes)
rails generate scaffold Post title:string body:text published:boolean

# Model only
rails generate model Comment body:text post:references user:references

# Controller only
rails generate controller Posts index show new create edit update destroy

# Migration only
rails generate migration AddStatusToUsers status:string

# Mailer
rails generate mailer UserMailer welcome_email

# Job
rails generate job SendEmail

# Shorthand: g = generate, s = scaffold, m = model, c = controller
rails g scaffold Article title:string content:text
rails g model Tag name:string
rails g c Dashboard index

Database commands

rails db:create          # Create database
rails db:migrate         # Run pending migrations
rails db:rollback        # Rollback last migration
rails db:rollback STEP=3 # Rollback 3 migrations
rails db:seed            # Run db/seeds.rb
rails db:reset           # Drop + create + migrate + seed
rails db:schema:load     # Load schema.rb (faster than migrate)
rails db:migrate:status  # Show migration status

# Run in specific environment
RAILS_ENV=production rails db:migrate

Other useful commands

rails routes                          # List all routes
rails routes | grep post              # Filter routes
rails routes --expanded               # Verbose format

rails console                         # IRB with app loaded
rails console --sandbox               # Changes rolled back on exit

rails test                            # Run all tests
rails test test/models/post_test.rb   # Run specific test file

rails credentials:edit                # Edit encrypted credentials
rails assets:precompile               # Compile assets (production)
rails middleware                      # List Rack middleware

Routing

Resource routes

# config/routes.rb

# RESTful resource (7 routes)
resources :posts

# Nested resources
resources :posts do
  resources :comments
end

# Shallow nesting (collection on parent, member on child)
resources :posts, shallow: true do
  resources :comments
end

# Only specific actions
resources :photos, only: [:index, :show]
resources :sessions, except: [:edit, :update]

# Single resource (no :id — current user, settings, etc.)
resource :profile
resource :session

Generated REST routes (resources :posts)

HTTP Verb Path Controller#Action Route Helper
GET /posts posts#index posts_path
GET /posts/new posts#new new_post_path
POST /posts posts#create posts_path
GET /posts/:id posts#show post_path(@post)
GET /posts/:id/edit posts#edit edit_post_path(@post)
PATCH/PUT /posts/:id posts#update post_path(@post)
DELETE /posts/:id posts#destroy post_path(@post)

Custom routes

# Named route
get "about", to: "pages#about", as: :about

# Route with param
get "posts/:year/:month", to: "posts#archive", as: :archive

# Namespace (admin routes + AdminController)
namespace :admin do
  resources :users
  resources :posts
end

# Scope (URL prefix without module)
scope "/api/v1" do
  resources :articles
end

# Member routes (action on specific record)
resources :posts do
  member do
    patch :publish
    patch :unpublish
  end
  collection do
    get :drafts
  end
end

# Root
root "home#index"

Active Record

Querying

# Find
Post.find(1)                         # Raises RecordNotFound if missing
Post.find_by(slug: "hello-world")    # Returns nil if missing
Post.find_by!(slug: "hello-world")   # Raises RecordNotFound if missing

# All / filtering
Post.all
Post.where(published: true)
Post.where("created_at > ?", 1.week.ago)
Post.where(status: ["draft", "review"])
Post.where.not(archived: true)

# Ordering / limiting
Post.order(created_at: :desc)
Post.order("title ASC, created_at DESC")
Post.limit(10).offset(20)
Post.first         # ORDER BY id ASC LIMIT 1
Post.last          # ORDER BY id DESC LIMIT 1

# Counting / existence
Post.count
Post.where(published: true).count
Post.exists?(id: 5)
Post.exists?(title: "Hello")

# Selecting specific columns
Post.select(:id, :title)
Post.pluck(:title)              # Array of values (no AR objects)
Post.pluck(:id, :title)         # Array of arrays

# Distinct
Post.distinct
Post.select(:category).distinct

# Grouping / aggregation
Post.group(:category).count     # { "tech" => 12, "news" => 5 }
Post.average(:rating)
Post.maximum(:views)
Post.minimum(:price)
Post.sum(:amount)

# Eager loading (avoid N+1)
Post.includes(:author, :tags)
Post.includes(comments: :author)
Post.eager_load(:author)        # Forces LEFT OUTER JOIN
Post.preload(:tags)             # Separate query

Creating records

# new + save
post = Post.new(title: "Hello", body: "World")
post.save          # returns true/false
post.save!         # raises ActiveRecord::RecordInvalid if invalid

# create (new + save combined)
Post.create(title: "Hello", body: "World")   # returns record (may be invalid)
Post.create!(title: "Hello", body: "World")  # raises on invalid

# first_or_create / find_or_create
user = User.find_or_create_by(email: "a@b.com")
tag  = Tag.first_or_create(name: "ruby")

Updating records

post = Post.find(1)
post.title = "New Title"
post.save

# update (find + assign + save)
post.update(title: "New", published: true)   # returns true/false
post.update!(title: "New")                   # raises on invalid

# update_all (skip callbacks & validations)
Post.where(draft: true).update_all(published: false)

# increment / decrement
post.increment!(:views)
post.decrement!(:stock)

Deleting records

post = Post.find(1)
post.destroy          # Runs callbacks (before_destroy, dependent: :destroy)
post.delete           # Skips callbacks — direct SQL DELETE

Post.where(archived: true).destroy_all   # Loads records, runs callbacks
Post.where(archived: true).delete_all    # Direct SQL, no callbacks

Migrations

# db/migrate/20260714000000_create_posts.rb
class CreatePosts < ActiveRecord::Migration[7.1]
  def change
    create_table :posts do |t|
      t.string  :title,       null: false
      t.text    :body
      t.string  :status,      default: "draft"
      t.boolean :published,   default: false, null: false
      t.integer :views_count, default: 0
      t.references :user, null: false, foreign_key: true

      t.timestamps  # adds created_at and updated_at
    end

    add_index :posts, :status
    add_index :posts, [:user_id, :created_at]
  end
end

Common migration methods

# Column operations
add_column    :posts, :subtitle, :string
remove_column :posts, :subtitle, :string
rename_column :posts, :body, :content
change_column :posts, :views, :bigint

# Null / default
change_column_null    :posts, :title, false
change_column_default :posts, :status, "draft"

# Index
add_index    :posts, :slug, unique: true
remove_index :posts, :slug

# Foreign key
add_foreign_key    :posts, :users
remove_foreign_key :posts, :users

# Table operations
create_table  :tags, id: false do |t| ... end
drop_table    :legacy_records
rename_table  :old_name, :new_name

# Column types
:string, :text, :integer, :bigint, :float, :decimal,
:boolean, :date, :datetime, :time, :binary, :json, :jsonb (PostgreSQL)

Models

Validations

class Post < ApplicationRecord
  # Presence
  validates :title, presence: true
  validates :body,  presence: true, length: { minimum: 10 }

  # Format
  validates :slug, format: { with: /\A[a-z0-9\-]+\z/, message: "only lowercase letters, numbers, dashes" }

  # Uniqueness
  validates :email, uniqueness: { case_sensitive: false }
  validates :slug,  uniqueness: { scope: :user_id }

  # Inclusion / exclusion
  validates :status, inclusion: { in: %w[draft published archived] }

  # Numericality
  validates :price, numericality: { greater_than: 0 }
  validates :age,   numericality: { only_integer: true, in: 18..120 }

  # Length
  validates :title,    length: { maximum: 255 }
  validates :password, length: { minimum: 8, maximum: 72 }

  # Confirmation
  validates :password, confirmation: true
  validates :password_confirmation, presence: true

  # Custom validator
  validate :publication_date_cannot_be_in_past

  private

  def publication_date_cannot_be_in_past
    if publish_at.present? && publish_at < Time.current
      errors.add(:publish_at, "can't be in the past")
    end
  end
end

Associations

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :comments, through: :posts
  has_one  :profile, dependent: :destroy
  has_many :followers, class_name: "Follow", foreign_key: :followed_id
end

class Post < ApplicationRecord
  belongs_to :user
  belongs_to :category, optional: true
  has_many   :comments, dependent: :destroy
  has_many   :taggings
  has_many   :tags, through: :taggings

  # Polymorphic
  has_many :images, as: :imageable
end

class Comment < ApplicationRecord
  belongs_to :post
  belongs_to :user

  # Polymorphic
  belongs_to :commentable, polymorphic: true
end

Scopes and callbacks

class Post < ApplicationRecord
  # Scopes
  scope :published, -> { where(published: true) }
  scope :recent,    -> { order(created_at: :desc) }
  scope :by_user,   ->(user) { where(user: user) }

  # Usage: Post.published.recent.by_user(current_user)

  # Callbacks
  before_validation :normalize_slug
  before_create     :set_default_status
  after_create      :send_notification
  after_commit      :invalidate_cache, on: [:create, :update]
  before_destroy    :check_if_deletable

  private

  def normalize_slug
    self.slug = title.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "") if title
  end

  def send_notification
    NotificationMailer.new_post(self).deliver_later
  end
end

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_post!, only: [:edit, :update, :destroy]

  def index
    @posts = Post.published.includes(:user).order(created_at: :desc).page(params[:page])
  end

  def show
    # @post set by before_action
  end

  def new
    @post = Post.new
  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 edit; end

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

  def destroy
    @post.destroy
    redirect_to posts_url, notice: "Post deleted."
  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_post!
    redirect_to root_path, alert: "Unauthorized" unless @post.user == current_user
  end
end

Rendering and redirecting

# HTML (default — renders app/views/posts/show.html.erb)
render :show

# With status
render :new, status: :unprocessable_entity

# JSON API
render json: @post
render json: { error: "Not found" }, status: :not_found
render json: PostSerializer.new(@post).serializable_hash

# Redirect
redirect_to @post               # post_path(@post) via polymorphic_url
redirect_to posts_path
redirect_to root_path, notice: "Done"
redirect_back fallback_location: root_path

# Head only (no body)
head :no_content
head :created, location: @post

Views

ERB templates

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

<% @posts.each do |post| %>
  <%= post.title %>
  <%# Comment — not rendered %>
<% end %>

<%# Link helpers %>
<%= link_to "Show", post_path(post) %>
<%= link_to "Edit", edit_post_path(post) %>
<%= link_to "Delete", post_path(post), data: { turbo_method: :delete, turbo_confirm: "Sure?" } %>

<%# Image %>
<%= image_tag post.cover_url, alt: post.title, class: "hero" %>

<%# Render partial %>
<%= render "post", post: post %>
<%= render @posts %>                   # Renders _post.html.erb for each

<%# Content for layout %>
<% content_for :title, post.title %>
<% content_for :head do %>
  <meta name="description" content="<%= post.excerpt %>">
<% end %>

Form helpers

<%# form_with (Rails 5.1+) %>
<%= form_with model: @post do |f| %>
  <% if @post.errors.any? %>
    <div class="errors">
      <% @post.errors.full_messages.each do |msg| %>
        <p><%= msg %></p>
      <% end %>
    </div>
  <% end %>

  <%= f.label :title %>
  <%= f.text_field :title, placeholder: "Post title", class: "input" %>

  <%= f.label :body %>
  <%= f.text_area :body, rows: 10 %>

  <%= f.label :category_id %>
  <%= f.select :category_id, Category.all.collect { |c| [c.name, c.id] }, include_blank: "Select…" %>

  <%= f.label :published %>
  <%= f.check_box :published %>

  <%# File upload %>
  <%= f.label :cover %>
  <%= f.file_field :cover, accept: "image/*" %>

  <%# Collection of checkboxes %>
  <%= f.collection_check_boxes :tag_ids, Tag.all, :id, :name %>

  <%= f.submit "Save Post", class: "btn-primary" %>
<% end %>

Authentication with Devise

# Gemfile
gem "devise"

rails generate devise:install
rails generate devise User
rails db:migrate

# Generate views to customize
rails generate devise:views
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :authenticate_user!   # Require login everywhere

  def after_sign_in_path_for(resource)
    dashboard_path
  end
end

# Skip auth for public pages
class HomeController < ApplicationController
  skip_before_action :authenticate_user!, only: [:index]
end
<%# Navigation %>
<% if user_signed_in? %>
  <%= current_user.email %>
  <%= link_to "Sign out", destroy_user_session_path, data: { turbo_method: :delete } %>
<% else %>
  <%= link_to "Sign in", new_user_session_path %>
  <%= link_to "Sign up", new_user_registration_path %>
<% end %>
# Add fields to Devise User (migration)
class AddNameToUsers < ActiveRecord::Migration[7.1]
  def change
    add_column :users, :name, :string
    add_column :users, :role, :string, default: "member"
  end
end

# Permit extra Devise params (application_controller.rb)
before_action :configure_permitted_parameters, if: :devise_controller?

def configure_permitted_parameters
  devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  devise_parameter_sanitizer.permit(:account_update, keys: [:name])
end

Background Jobs

Active Job (built-in)

# app/jobs/send_welcome_email_job.rb
class SendWelcomeEmailJob < ApplicationJob
  queue_as :default
  retry_on Net::OpenTimeout, wait: 5.seconds, attempts: 3
  discard_on ActiveJob::DeserializationError

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

# Enqueue
SendWelcomeEmailJob.perform_later(user.id)
SendWelcomeEmailJob.set(wait: 1.hour).perform_later(user.id)
SendWelcomeEmailJob.set(wait_until: Date.tomorrow.noon).perform_later(user.id)

# config/application.rb — choose adapter
config.active_job.queue_adapter = :sidekiq  # or :solid_queue, :good_job

Action Mailer

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  default from: "noreply@example.com"

  def welcome(user)
    @user = user
    @login_url = new_user_session_url
    mail(to: @user.email, subject: "Welcome to MyApp!")
  end

  def password_reset(user, token)
    @user = user
    @token = token
    mail(to: @user.email, subject: "Reset your password")
  end
end

# app/views/user_mailer/welcome.html.erb
# app/views/user_mailer/welcome.text.erb

# Send
UserMailer.welcome(@user).deliver_now
UserMailer.welcome(@user).deliver_later

Caching

# Fragment caching in views
<% cache @post do %>
  <%= render @post %>
<% end %>

# Cache with dependencies
<% cache [@post, current_user] do %>
  <%= render "post_with_actions", post: @post %>
<% end %>

# Low-level caching
Rails.cache.fetch("post_#{id}", expires_in: 1.hour) do
  Post.find(id)
end

Rails.cache.write("key", value, expires_in: 30.minutes)
Rails.cache.read("key")
Rails.cache.delete("key")

# Counter cache
belongs_to :post, counter_cache: true
# Adds comments_count column to posts — auto-maintained

# config/environments/production.rb
config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }

Common patterns

API controller

# app/controllers/api/v1/posts_controller.rb
module Api
  module V1
    class PostsController < ApplicationController
      skip_before_action :verify_authenticity_token
      before_action :authenticate_api_token!

      def index
        @posts = Post.published.order(created_at: :desc)
        render json: @posts.as_json(only: [:id, :title, :slug, :created_at])
      end

      def show
        @post = Post.find(params[:id])
        render json: @post
      rescue ActiveRecord::RecordNotFound
        render json: { error: "Not found" }, status: :not_found
      end

      private

      def authenticate_api_token!
        token = request.headers["Authorization"]&.split(" ")&.last
        @current_api_user = User.find_by(api_token: token)
        render json: { error: "Unauthorized" }, status: :unauthorized unless @current_api_user
      end
    end
  end
end

Pagination with Pagy

# Gemfile
gem "pagy"

# app/controllers/application_controller.rb
include Pagy::Backend

# Controller
def index
  @pagy, @posts = pagy(Post.published.order(created_at: :desc), items: 20)
end

# View helper
include Pagy::Frontend

# app/views/posts/index.html.erb
<%== pagy_nav(@pagy) %>

Service object pattern

# app/services/publish_post_service.rb
class PublishPostService
  def initialize(post, publisher)
    @post = post
    @publisher = publisher
  end

  def call
    return Result.failure("Already published") if @post.published?
    return Result.failure("Not authorized") unless authorized?

    ActiveRecord::Base.transaction do
      @post.update!(published: true, published_at: Time.current)
      NotificationJob.perform_later(@post.id)
      AuditLog.create!(action: "publish", record: @post, user: @publisher)
    end

    Result.success(@post)
  rescue ActiveRecord::RecordInvalid => e
    Result.failure(e.message)
  end

  private

  def authorized?
    @publisher.admin? || @post.user == @publisher
  end

  Result = Data.define(:success, :value, :error) do
    def self.success(value) = new(success: true,  value: value, error: nil)
    def self.failure(error) = new(success: false, value: nil,   error: error)
    def success? = success
  end
end

# Usage in controller
result = PublishPostService.new(@post, current_user).call
if result.success?
  redirect_to @post, notice: "Published"
else
  redirect_to @post, alert: result.error
end

Common mistakes

Mistake Problem Fix
N+1 queries `@posts.each { p
Missing ! on save Silent failure — form re-renders empty Use create! in console/seeds, save/update in controllers
Exposing all params params[:post] without permit Always use Strong Parameters: params.require(:post).permit(...)
Callback hell 10 chained callbacks — untestable Extract to service objects for complex logic
Synchronous email deliver_now blocks request for 2–5s deliver_later with Sidekiq/GoodJob
update_all skips validations Invalid data silently saved Use only for bulk updates where validations are irrelevant
Forgetting dependent: Deleting User leaves orphan Posts Add dependent: :destroy or :nullify to associations
Direct find without rescue Unhandled RecordNotFound → 500 error Use find_by + nil check, or rescue in controller

Rails vs Django vs Laravel

Feature Rails Django Laravel
Language Ruby Python PHP
Convention Strong (CoC) Moderate Strong
ORM Active Record Django ORM Eloquent
Migration style Code-first Code-first Code-first
Auth Devise gem Built-in Laravel Breeze/Jetstream
Admin panel ActiveAdmin gem Built-in No default
API mode --api flag DRF Built-in
Async/jobs ActiveJob + Sidekiq Celery Laravel Queues
Testing Minitest (default) unittest PHPUnit

FAQ

What is "Convention over Configuration" in Rails? Rails assumes sensible defaults — a posts table maps to the Post model, app/views/posts/index.html.erb renders for PostsController#index — so you only configure when you deviate. This reduces boilerplate dramatically.

What's the difference between find and find_by? find(id) raises ActiveRecord::RecordNotFound if the record is missing. find_by(conditions) returns nil. Use find when you expect the record to exist; use find_by when absence is a valid case.

How do I prevent N+1 queries? Use includes(:association) to eager-load associations. Install the bullet gem in development — it alerts you to N+1 queries and unused eager loads automatically.

What is Strong Parameters and why does Rails require it? Strong Parameters (params.require(:post).permit(:title, :body)) prevents mass assignment vulnerabilities where attackers submit extra fields like role: "admin". Rails raises ForbiddenAttributesError if you pass unpermitted params directly to create/update.

When should I use a service object vs a model method? Model methods are ideal for single-record business logic tightly coupled to that model. Service objects (app/services/) are better for multi-model operations, external API calls, complex workflows, or any logic that's hard to test in isolation.

How do I run Rails in production? Use Puma (Rails default web server), Sidekiq or GoodJob for jobs, PostgreSQL for the database, and a reverse proxy (Nginx or Caddy). Set RAILS_ENV=production, run rails assets:precompile and rails db:migrate, then start with bundle exec puma -C config/puma.rb.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools