DevTools

Cheatsheet Ruby on Rails

Framework web Ruby full-stack com convenção sobre configuração

Back to languages
Ruby on Rails
75 cards found
Categories:
Versions:

Setup e CLI


9 cards
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


9 cards
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.recent

scope 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
end

Callbacks 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 published

enum 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


9 cards
RESTful Actions
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end

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

The 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)
end

params.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
end

rescue_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
end

respond_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


8 cards
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/archived

member 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


9 cards
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


8 cards
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
end

def 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:seed

db/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


8 cards
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 @post

Pundit 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::Attack

Rails 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


7 cards
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)
end

cache 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
end

jsonapi-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
end

Concerns 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_later

Action 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


8 cards
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
end

Minitest 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
end

System 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
end

Integration 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
end

Fixtures 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
end

RSpec 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.inspect

Rails.logger writes to log/development.log. debugger halts execution for interactive inspection. params.inspect shows all received parameters.