Cheatsheet Ruby on Rails
Framework web Ruby full-stack com convenção sobre configuração
Ruby on Rails
Setup e CLI
Install Rails
gem install rails rails new my_app cd my_app rails server
gem install rails installs the framework globally. rails new generates the full project structure. rails server starts Puma on localhost:3000.
Essential CLI Commands
rails console # IRB with the app loaded rails db:migrate # apply migrations rails db:seed # seed the DB rails routes # list all routes rails dbconsole # direct SQL access
The rails console loads the whole environment (models, configs). rails routes shows mapped verbs, paths and controllers. dbconsole opens the native SQL client.
Configuration
# config/application.rb config.time_zone = "Lisbon" config.i18n.default_locale = :en # config/credentials.yml.enc rails credentials:edit Rails.application.credentials.secret_api_key
config/application.rb defines global settings. credentials stores encrypted secrets (API keys, passwords) — safer than environment variables in a file.
Generate Model
rails g model Post title:string body:text rails g model User name:string email:string # Generates: # app/models/post.rb # db/migrate/xxxx_create_posts.rb
rails g model creates the model file and the migration automatically. The types (string, text, integer) define the table columns.
Project Structure
app/ models/ # ActiveRecord controllers/ # HTTP logic views/ # ERB templates helpers/ # view helpers config/routes.rb # routes db/migrate/ # migrations Gemfile # dependencies
Rails follows the MVC pattern: models/ (data), controllers/ (logic), views/ (presentation). config/routes.rb maps URLs to controllers.
Generate Controller
rails g controller Posts index show new # Generates: # app/controllers/posts_controller.rb # app/views/posts/ (index, show, new) # routes in config/routes.rb
rails g controller creates the controller, the views and adds the routes. The listed actions generate empty methods and matching templates.
Gemfile
# Gemfile gem "rails", "~> 7.1" gem "sqlite3" gem "puma" group :development do gem "debug" end bundle install bundle add devise
The Gemfile lists the dependencies. bundle install installs everything. group :development limits gems to specific environments. bundle add adds and installs in one step.
Scaffold
rails g scaffold Post title:string body:text # Generates everything: model, controller, # views, migration, routes, tests rails db:migrate
scaffold generates the full CRUD at once: model, controller with 7 actions, views, migration and routes. Ideal for fast prototyping.
Environments
# config/environments/ # development.rb # test.rb # production.rb RAILS_ENV=production rails server Rails.env # => "development" Rails.env.production? # => false
Rails has 3 environments: development, test and production. Each one has its own configs in config/environments/. Rails.env returns the active environment.
Models e ActiveRecord
Define a Model
class Post < ApplicationRecord validates :title, presence: true belongs_to :author has_many :comments end
Every model inherits from ApplicationRecord. The class name is singular (Post) and the table is plural (posts). validates and associations are declared in the class body.
Associations
has_many :comments belongs_to :post has_one :profile has_many :tags, through: :taggings has_and_belongs_to_many :categories
has_many / belongs_to are the most common. through creates an association via an intermediate table. has_and_belongs_to_many uses a join table without its own model.
Eager Loading
# N+1 problem:
Post.all.each { |p| p.author.name }
# Solution:
Post.includes(:author).each { |p|
p.author.name }
Post.joins(:author).where(
authors: { active: true })includes loads associations in 2 queries (avoids N+1). joins does an INNER JOIN — required to filter by association fields. preload forces 2 separate queries.
Validations
validates :email, presence: true,
uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :age, numericality: {
greater_than: 0, only_integer: true }
validates :title, length: { maximum: 200 }validates accepts multiple rules: presence, uniqueness, format, numericality, length. If it fails, save returns false and errors is populated.
Scopes
scope :active, -> { where(active: true) }
scope :recent, -> {
order(created_at: :desc).limit(5) }
Post.active.recentscope defines reusable named queries with a lambda. They can be chained like normal methods. Equivalent to class methods that return an ActiveRecord::Relation.
Basic CRUD
Post.create(title: "Hello", body: "World") Post.find(1) Post.find_by(title: "Hello") post.update(title: "New title") post.destroy
create instantiates and saves. find raises RecordNotFound if it does not exist; find_by returns nil. update validates and saves. destroy removes the record.
Callbacks
class Post < ApplicationRecord
before_save :normalize_title
after_create :notify
private
def normalize_title
self.title = title.strip.downcase
end
endCallbacks run code at lifecycle points: before_save, after_create, before_destroy. Useful for normalization, logs and automatic notifications.
Queries and Conditions
Post.where(active: true)
Post.where("views > ?", 100)
Post.order(created_at: :desc)
Post.limit(10).offset(20)
Post.count
Post.pluck(:title)where accepts a hash or SQL with ? placeholders. Queries are chainable (lazy). pluck returns only the values of one column. count runs SELECT COUNT(*).
Enum
class Post < ApplicationRecord
enum :status, { draft: 0,
published: 1, archived: 2 }
end
post.published!
post.status # => "published"
Post.published # => all publishedenum maps integers to readable names. It generates query methods (Post.published) and transition methods (post.published!). The column must be integer in the DB.
Controllers
RESTful Actions
class PostsController < ApplicationController
def index
@posts = Post.all
end
def show
@post = Post.find(params[:id])
end
endThe 7 RESTful actions: index, show, new, create, edit, update, destroy. Variables with @ become available in the view.
Flash and Redirect
flash[:notice] = "Post created!" flash[:alert] = "Error while saving." redirect_to posts_path redirect_to @post, notice: "OK" redirect_to root_path, alert: "Denied"
flash stores messages for the next request. notice (success) and alert (error) are inline shortcuts in redirect_to. The message disappears after being displayed.
Skip and Inheritance
class ApplicationController < ActionController::Base before_action :authenticate! end class PublicController < ApplicationController skip_before_action :authenticate! end
ApplicationController is the parent of all — filters defined here apply globally. skip_before_action removes an inherited filter in specific controllers.
Strong Parameters
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: "Created!"
else
render :new, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title, :body)
endparams.require(:post) requires the key. permit lists the accepted fields — protects against mass assignment. Without it, ActiveModel::ForbiddenAttributesError is raised.
Render vs Redirect
# Render: same request, no redirect render :new render partial: "form" render plain: "OK" # Redirect: new HTTP request redirect_to posts_path
render shows a view without changing the URL (same request). redirect_to sends HTTP 302 — the browser does a new GET. Use render on validation errors to keep form data.
Before Actions
before_action :set_post, only: [:show, :edit, :update, :destroy] before_action :authenticate! private def set_post @post = Post.find(params[:id]) end
before_action runs before the actions. only / except limit where it runs. Ideal for loading resources and checking authentication without repeating code.
Rescue and Errors
rescue_from ActiveRecord::RecordNotFound,
with: :not_found
private
def not_found
render file: "public/404.html",
status: :not_found
endrescue_from catches exceptions globally in the controller. Avoids repeating begin/rescue. It can render error pages or redirect with messages.
Respond_to
def show
@post = Post.find(params[:id])
respond_to do |format|
format.html
format.json { render json: @post }
format.xml { render xml: @post }
end
endrespond_to allows responding in multiple formats based on the Accept header. The block defines the response for each format. The URL extension (.json) also triggers it.
Cookies and Session
session[:user_id] = user.id session[:user_id] # => 42 session.delete(:user_id) cookies[:theme] = "dark" cookies.permanent[:remember] = token
session stores temporary data (cleared when the browser closes). cookies persists on the client. cookies.permanent expires in 20 years. Both accessible the a hash.
Routes
Resources
resources :posts # Generates 7 routes: index, show, new, # create, edit, update, destroy resources :posts, only: [:index, :show] resources :posts, except: [:destroy]
resources generates the 7 RESTful routes automatically. only / except limit which are created. Each route has a named helper (posts_path, post_path).
Namespace
namespace :admin do resources :users resources :posts end # /admin/users # Admin::UsersController
namespace groups routes under a prefix and module. It creates controllers in app/controllers/admin/. Helpers get a prefix: admin_users_path.
Custom Routes
get "about", to: "pages#about" post "login", to: "sessions#create" root "posts#index" get "posts/:id/preview", to: "posts#preview"
get, post, put, delete define manual routes. root sets the home page. :id is a dynamic parameter accessible via params[:id].
Scope and Shallow
scope "/api", module: "api" do resources :posts end resources :posts, shallow: true do resources :comments end # /comments/5 (without /posts/1)
scope customizes prefix/module without creating a full namespace. shallow: true nests only index/new/create — the rest stay at root level.
Nested Resources
resources :posts do resources :comments end # /posts/1/comments # /posts/1/comments/5
Nested resources generate hierarchical URLs. The controller receives params[:post_id]. Limit nesting to 1 level to keep URLs readable.
Member and Collection
resources :posts do
member do
get :preview
end
collection do
get :archived
end
end
# /posts/1/preview
# /posts/archivedmember adds routes for a specific resource (with :id). collection adds routes for the collection (without :id). They generate automatic helpers.
URL Helpers
posts_path # /posts post_path(@post) # /posts/1 new_post_path # /posts/new edit_post_path(@post) # /posts/1/edit posts_url # http://host/posts
Each route generates helpers: _path (relative) and _url (absolute). Use _path in views and _url in emails and external redirects.
Constraints
constraints subdomain: "api" do
resources :posts
end
get "posts/:id", to: "posts#show",
constraints: { id: /\d+/ }constraints restricts routes by subdomain, format or regex. Useful for APIs on subdomains or for validating parameters directly in the route.
Views e ERB
ERB Basics
<h1><%= @post.title %></h1> <p><%= @post.body %></p> <%# comment (not rendered) %> <% if @post.active? %> <span>Published</span> <% end %>
<%= %> prints the result (with HTML escaping). <% %> executes without printing. <%# %> is a comment. @ variables from the controller are available.
Layouts and Yield
<!-- layouts/application.html.erb --> <body> <%= yield %> <%= yield :sidebar %> </body> <!-- In the view: --> <% content_for :sidebar do %> <nav>Side menu</nav> <% end %>
yield inserts the view content into the layout. yield :sidebar inserts named content. content_for defines blocks for specific layout slots.
Turbo and Stimulus
<%= turbo_frame_tag "posts" do %>
<%= render @posts %>
<% end %>
<%= link_to "Next", posts_path,
data: { turbo_frame: "posts" } %>Turbo Frames update parts of the page without a full reload. turbo_frame_tag defines an updatable region. Links with data-turbo-frame load inside the frame.
Iteration
<% @posts.each do |post| %> <li><%= link_to post.title, post %></li> <% end %> <%= render @posts %>
each iterates over collections. render @posts automatically renders the _post.html.erb partial for each item — cleaner than a manual loop.
Asset Helpers
<%= image_tag "logo.png", alt: "Logo" %> <%= stylesheet_link_tag "app" %> <%= javascript_include_tag "app" %> <%= link_to "View post", post_path(@post) %>
image_tag, stylesheet_link_tag and javascript_include_tag generate tags with asset pipeline paths. link_to creates links with route helpers.
Partials
<%= render "form" %> <%= render "posts/card", post: @post %> <!-- app/views/posts/_card.html.erb --> <div class="card"> <%= post.title %> </div>
Partials are files prefixed with _. render "form" looks for _form.html.erb in the same folder. Pass local variables the the second argument (hash).
Number and Date Helpers
<%= number_to_currency(19.99) %> <%= number_with_delimiter(1000000) %> <%= time_ago_in_words(@post.created_at) %> <%= l(@post.created_at, format: :long) %>
number_to_currency formats monetary values. time_ago_in_words returns "3 days ago". l() formats dates according to the active locale.
Form Helpers
<%= form_with model: @post do |f| %> <%= f.label :title %> <%= f.text_field :title %> <%= f.text_area :body, rows: 5 %> <%= f.select :status, ["draft", "published"] %> <%= f.submit "Save" %> <% end %>
form_with model: generates a form for create or update automatically. f.text_field, f.text_area, f.select create inputs with the right names. The CSRF token is included automatically.
Content Tags and Safe
<%= content_tag :div, "Hello", class: "msg" %> <%= tag.br %> <%= tag.input type: "text", name: "q" %> <%= raw @html %> <%= sanitize @html, tags: %w[b i em] %>
content_tag generates HTML elements dynamically. tag.br creates self-closing tags. raw prints without escaping (careful!). sanitize removes dangerous tags while keeping safe ones.
Migrations
Create a Migration
rails g migration CreatePosts \ title:string body:text active:boolean rails g migration AddViewsToPosts \ views:integer
rails g migration generates a file with a timestamp. CreateX names generate create_table. AddXToY names generate add_column. The timestamp defines execution order.
Reversible Change
def change
create_table :posts do |t|
t.string :title, null: false
t.text :body
t.timestamps
end
enddef change is auto-reversible — Rails knows how to invert create_table, add_column, etc. Use def up / def down only for irreversible operations.
Column Types
t.string :title t.text :body t.integer :views t.float :rating t.decimal :price, precision: 8, scale: 2 t.boolean :active, default: false t.date :published_at t.datetime :created_at t.references :author, foreign_key: true
Types map to SQL: string (varchar), text (no limit), decimal (exact precision). references creates an author_id column with a foreign key.
Timestamps and Defaults
t.timestamps # Creates: created_at + updated_at t.boolean :active, default: true t.string :status, default: "draft" t.integer :views, default: 0
t.timestamps creates created_at and updated_at (updated automatically). default sets the column initial value. Rails manages timestamps with no extra code.
Alter Schema
add_column :posts, :views, :integer remove_column :posts, :views rename_column :posts, :old, :new add_index :posts, :title change_column_null :posts, :title, false
add_column / remove_column add/remove columns. add_index improves query performance. change_column_null makes a column required (NOT NULL).
Indexes and Constraints
add_index :posts, :title, unique: true add_index :posts, [:author_id, :created_at] add_foreign_key :posts, :authors add_check_constraint :posts, "views >= 0", name: "positive_views"
add_index with unique: true prevents duplicates. Composite indexes (array) optimize queries with multiple columns. add_check_constraint validates at the DB level.
Management Commands
rails db:migrate rails db:rollback STEP=1 rails db:migrate:status rails db:reset rails db:schema:load
db:migrate applies pending migrations. db:rollback reverts the last one (or N with STEP). migrate:status shows which were applied. db:reset does drop + create + migrate.
Data Seeding
# db/seeds.rb
Post.create!(title: "First",
body: "Initial content")
5.times do |i|
Post.create!(title: "Post #{i}")
end
# Run:
rails db:seeddb/seeds.rb populates the DB with initial data. create! (with bang) raises an exception if validation fails. Run with rails db:seed — safe for idempotency.
Auth and Security
has_secure_password
gem "bcrypt"
class User < ApplicationRecord
has_secure_password
end
user.authenticate("password123")has_secure_password adds bcrypt hashing and the authenticate method. Requires a password_digest column. Returns the user if the password matches, false otherwise.
Pundit (Authorization)
gem "pundit"
class PostPolicy < ApplicationPolicy
def update?
user.admin? || record.author == user
end
end
authorize @postPundit handles authorization with policies. Each model has a Policy class with boolean methods. authorize in the controller checks permission — raises NotAuthorizedError if denied.
Devise
gem "devise" rails g devise:install rails g devise User rails db:migrate # Available helpers: current_user user_signed_in? authenticate_user!
Devise is the most popular auth gem. It generates sign-up, login, password recovery and email confirmation. authenticate_user! the a before_action protects routes.
SQL Injection
# SAFE (placeholder):
Post.where("title LIKE ?", "%#{q}%")
# DANGEROUS (never do this):
Post.where("title LIKE '%#{q}%'")
# SAFE (hash):
Post.where(title: params[:title])Always use ? placeholders or a hash in where. Direct interpolation allows SQL injection. ActiveRecord escapes automatically with placeholders.
CSRF Protection
# ApplicationController protect_from_forgery with: :exception # In the layout (automatic): <%= csrf_meta_tags %> # In forms: token included
Rails protects against CSRF by default. csrf_meta_tags inserts the token into the HTML. Forms generated with form_with include the token automatically. APIs use skip_forgery_protection.
Security Headers
# config/application.rb
config.action_dispatch.default_headers = {
"X-Frame-Options" => "DENY",
"X-Content-Type-Options" => "nosniff",
"X-XSS-Protection" => "1; mode=block"
}Rails already includes secure headers by default. X-Frame-Options prevents clickjacking. X-Content-Type-Options avoids MIME sniffing. Customize in default_headers.
XSS and Sanitize
<%= @text %> # escapes HTML <%= raw @html %> # does NOT escape <%= sanitize @html, tags: %w[b i em p] %>
ERB escapes HTML automatically with <%= %>. raw disables escaping (dangerous). sanitize removes malicious tags/scripts while keeping a safe whitelist.
Rate Limiting
# config/initializers/rate_limit.rb
Rails.application.config.action_dispatch
.rate_limit = {
limit: 100,
period: 1.minute
}
# Or manually with Rack::AttackRails 7.1+ has native rate limiting. It limits requests per IP/period. For fine-grained control, use Rack::Attack (gem) with rules per endpoint, token or IP.
API and Advanced
API Mode
rails new api_app --api
class PostsController < ApplicationController
def index
render json: Post.all
end
def show
render json: Post.find(params[:id])
end
end--api creates an app without views, assets or cookies. Controllers inherit from ActionController::API (lighter). render json: serializes objects automatically.
Caching
# Fragment caching in the view:
<% cache @post do %>
<%= render @post %>
<% end %>
# Low-level caching:
Rails.cache.fetch("popular", expires_in: 1.hour) do
Post.order(views: :desc).limit(10)
endcache in the view stores rendered HTML (invalidated on update). Rails.cache.fetch stores any data with expiration. Use Redis the the store in production.
Serializers
gem "jsonapi-serializer"
class PostSerializer
include JSONAPI::Serializer
attributes :title, :body
belongs_to :author
attribute :url do |post|
post_url(post)
end
endjsonapi-serializer formats responses in the JSON:API standard. attributes lists fields. attribute with a block creates computed fields. Faster than manual to_json.
Action Cable (WebSockets)
rails g channel Notifications
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications"
end
end
ActionCable.server.broadcast(
"notifications", { msg: "Hello" })Action Cable integrates WebSockets into Rails. stream_from subscribes to a channel. broadcast sends messages to all subscribers. Ideal for real-time notifications.
Active Job
rails g job SendEmail
class SendEmailJob < ApplicationJob
queue_as :default
def perform(user)
UserMailer.welcome(user).deliver_now
end
end
SendEmailJob.perform_later(user)Active Job is the interface for background jobs. perform_later enqueues (non-blocking). queue_as sets the queue. Use Sidekiq or Solid Queue the the backend.
Engines and Concerns
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
included do
scope :search, ->(q) {
where("title LIKE ?", "%#{q}%") }
end
end
class Post < ApplicationRecord
include Searchable
endConcerns are reusable modules (mixins). ActiveSupport::Concern enables included hooks. They keep models DRY. Engines are packageable mini-apps (internal gems).
Action Mailer
rails g mailer UserMailer
def welcome(user)
@user = user
mail(to: user.email,
subject: "Welcome!")
end
# app/views/user_mailer/welcome.html.erb
UserMailer.welcome(user).deliver_laterAction Mailer sends emails with ERB templates. deliver_later sends via background job. Configure SMTP in config/environments/production.rb or use ActionMailer::Base.delivery_method.
Testing and Deployment
Minitest (Unit)
class PostTest < ActiveSupport::TestCase
test "title is required" do
post = Post.new(title: nil)
assert_not post.valid?
assert_includes post.errors[:title],
"can't be blank"
end
endMinitest ships with Rails. test "..." defines a case. assert_not checks falsity. assert_includes confirms presence in an array. Run with rails test.
System Tests
class PostsSystemTest < ApplicationSystemTestCase
test "create post via browser" do
visit new_post_path
fill_in "Title", with: "New"
click_button "Save"
assert_text "Post created!"
end
endSystem tests run in a real browser (Capybara). visit, fill_in, click_button simulate user interaction. They test the full flow (front + back).
Integration Tests
class PostsFlowTest < ActionDispatch::IntegrationTest
test "create post" do
post posts_path, params: {
post: { title: "New" } }
assert_response :redirect
follow_redirect!
assert_select "h1", "New"
end
endIntegration tests simulate full HTTP requests. post posts_path sends a request. assert_response checks the status. assert_select asserts on the returned HTML.
Prepare for Production
RAILS_ENV=production rails db:migrate rails assets:precompile rails secret # config/environments/production.rb config.force_ssl = true config.log_level = :info
assets:precompile compiles CSS/JS with fingerprints. force_ssl forces HTTPS. rails secret generates secret_key_base. Logs at :info reduce noise.
Fixtures and Factories
# test/fixtures/posts.yml
first:
title: "Hello"
body: "World"
# With FactoryBot (gem):
FactoryBot.define do
factory :post do
title { "Test" }
end
endFixtures are static YAML data. FactoryBot generates dynamic data with create(:post). Factories are more flexible and avoid coupling between tests.
Deploy (Capistrano)
gem "capistrano-rails" # config/deploy.rb set :application, "my_app" set :repo_url, "git@github.com:user/app.git" set :deploy_to, "/var/www/app" cap production deploy
Capistrano automates deploys over SSH. It pulls from git, runs bundle install, migrations and restart. Supports rollback with cap production deploy:rollback.
RSpec (Alternative)
RSpec.describe Post do
it "requires title" do
post = Post.new(title: nil)
expect(post).not_to be_valid
end
it "creates with title" do
post = Post.create(title: "OK")
expect(post).to be_persisted
end
endRSpec is the most popular alternative to Minitest. describe/it/expect syntax. Matchers like be_valid, eq, include make tests readable.
Logs and Debugging
Rails.logger.info("Processing...")
Rails.logger.error("Failure: #{e.message}")
# In the controller/view:
debugger # halts execution (debug gem)
logger.debug params.inspectRails.logger writes to log/development.log. debugger halts execution for interactive inspection. params.inspect shows all received parameters.