Ruby on Rails Basics Cheat Sheet
Covers the Rails CLI, MVC routing and controllers, ActiveRecord models, and core Rails naming conventions for building web apps.
Rails CLI
Essential commands for scaffolding and running a Rails app.
rails new myapp --database=postgresql # Create new appcd myapprails generate model Post title:string body:text # Generate model + migrationrails generate controller Posts index show # Generate controllerrails db:create # Create databaserails db:migrate # Run migrationsrails server # Start dev server (localhost:3000)rails console # Interactive REPL
Routes & Controllers
Defining RESTful routes and a resourceful controller.
# config/routes.rbRails.application.routes.draw do resources :posts # generates index/show/new/create/edit/update/destroy root "posts#index"end# app/controllers/posts_controller.rbclass PostsController < ApplicationController def index @posts = Post.all end def show @post = Post.find(params[:id]) 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 post_params params.require(:post).permit(:title, :body) endend
Models & ActiveRecord
Associations, validations, and common query methods.
# app/models/post.rbclass Post < ApplicationRecord belongs_to :author, class_name: "User" has_many :comments, dependent: :destroy validates :title, presence: true, length: { maximum: 100 }end# ActiveRecord query examplesPost.where(published: true).order(created_at: :desc).limit(5)Post.find_by(title: "Hello World")post.comments.create(body: "Nice post!")
MVC Conventions
Naming and structure conventions Rails relies on.
- Model naming- Singular, CamelCase class name (Post) mapped to a plural, snake_case table (posts)
- Controller naming- Plural, CamelCase class name ending in Controller (PostsController) under app/controllers
- Migrations- Timestamped files in db/migrate/, applied in order via rails db:migrate
- Convention over configuration- Rails infers foreign keys (post_id), table names, and view paths automatically from class names
- Views- app/views/<controller>/<action>.html.erb renders by default, matching the action name
- Gemfile / Bundler- Gemfile plus bundle install manage gem dependencies; config/environments/ holds per-environment settings
Scopes & N+1 Avoidance
Defining reusable query scopes and eager-loading associations to prevent N+1 queries.
# app/models/post.rbclass Post < ApplicationRecord scope :published, -> { where(published: true) } scope :recent, ->(limit = 5) { order(created_at: :desc).limit(limit) }end# Chaining scopesPost.published.recent(10)# N+1 problem: fires one query per post for authorPost.published.each { |post| puts post.author.name }# Fixed with eager loadingPost.published.includes(:author, :comments).each do |post| puts "#{post.author.name}: #{post.comments.size} comments"end# Bullet gem (dev/test) flags N+1s automatically when configured
Concerns & Model Callbacks
Sharing behavior across models with ActiveSupport::Concern and hooking into the record lifecycle.
# app/models/concerns/sluggable.rbmodule Sluggable extend ActiveSupport::Concern included do before_validation :generate_slug validates :slug, uniqueness: true end private def generate_slug self.slug ||= title.to_s.parameterize endend# app/models/post.rbclass Post < ApplicationRecord include Sluggable before_save :normalize_title after_create_commit :notify_subscribers private def normalize_title self.title = title.strip end def notify_subscribers SubscriberMailer.new_post(self).deliver_later endend
Background Jobs with ActiveJob
Offloading slow work to a queue instead of blocking the request cycle.
# app/jobs/thumbnail_job.rbclass ThumbnailJob < ApplicationJob queue_as :default retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 5 def perform(post_id) post = Post.find(post_id) post.generate_thumbnail! endend# Enqueue from a controller or model callbackThumbnailJob.perform_later(post.id)# Run immediately, synchronously (e.g. in tests)ThumbnailJob.perform_now(post.id)# config/application.rb — pick a backend (Sidekiq, Solid Queue, etc.)# config.active_job.queue_adapter = :sidekiq
Advanced Rails Concepts
Patterns you'll hit once an app grows past CRUD basics.
- Polymorphic associations- belongs_to :commentable, polymorphic: true lets one model (Comment) belong to several other models (Post, Photo) via a type + id column pair
- has_many :through- Models a many-to-many relationship via an explicit join model, e.g. has_many :taggings then has_many :tags, through: :taggings
- Single Table Inheritance (STI)- Subclasses share one table via a `type` column, e.g. Admin < User, queried transparently through the base class
- Service objects- Plain Ruby classes (app/services/) encapsulating multi-step business logic that doesn't belong in a fat model or controller
- Rails credentials- bin/rails credentials:edit stores encrypted secrets in config/credentials.yml.enc, decrypted at boot via config/master.key
- Strong migrations- Avoid unsafe ops on large tables (adding a column with a default, adding a NOT NULL constraint) without a gem like strong_migrations flagging them
- API mode- rails new app --api strips view/asset middleware for a lean JSON-only backend
Transactions & Locking
Wrapping multi-step writes atomically and guarding against concurrent updates.
# Atomic multi-step write — rolls back everything on any exceptionActiveRecord::Base.transaction do order = Order.create!(user: user, total: cart.total) cart.items.each { |item| order.line_items.create!(item.attributes) } Inventory.decrement!(cart.items)end# Optimistic locking: add a `lock_version` integer column# Raises ActiveRecord::StaleObjectError if another process updated the row firstclass Post < ApplicationRecordendpost.update!(title: "New title") # StaleObjectError if lock_version mismatches# Pessimistic locking: SELECT ... FOR UPDATE inside a transactionActiveRecord::Base.transaction do account = Account.lock.find(id) account.update!(balance: account.balance - amount)end
Always mass-assign through strong parameters (params.require(:model).permit(...)) in every controller action — skipping it opens the door to mass-assignment vulnerabilities.