Cheatsheet Ruby on Rails
Framework web Ruby full-stack com convenção sobre configuração
Ruby on Rails
Setup e CLI
Instalar Rails
gem install rails rails new minha_app cd minha_app rails server
gem install rails instala o framework globalmente. rails new gera a estrutura completa do projecto. rails server inicia o Puma em localhost:3000.
Comandos CLI essenciais
rails console # IRB com o app carregado rails db:migrate # aplicar migrations rails db:seed # popular BD rails routes # listar todas as rotas rails dbconsole # aceder ao SQL directo
A rails console carrega todo o ambiente (models, configs). rails routes mostra verbos, paths e controllers mapeados. dbconsole abre o cliente SQL nativo.
Configurações
# config/application.rb config.time_zone = "Lisbon" config.i18n.default_locale = :pt # config/credentials.yml.enc rails credentials:edit Rails.application.credentials.secret_api_key
config/application.rb define settings globais. credentials guarda segredos encriptados (API keys, passwords) — mais seguro que variáveis de ambiente em ficheiro.
Gerar model
rails g model Post titulo:string texto:text rails g model User nome:string email:string # Gera: # app/models/post.rb # db/migrate/xxxx_create_posts.rb
rails g model cria o ficheiro do model e a migration automaticamente. Os tipos (string, text, integer) definem as colunas da tabela.
Estrutura do projecto
app/ models/ # ActiveRecord controllers/ # lógica HTTP views/ # templates ERB helpers/ # view helpers config/routes.rb # rotas db/migrate/ # migrations Gemfile # dependências
Rails segue o padrão MVC: models/ (dados), controllers/ (lógica), views/ (apresentação). config/routes.rb mapeia URLs para controllers.
Gerar controller
rails g controller Posts index show new # Gera: # app/controllers/posts_controller.rb # app/views/posts/ (index, show, new) # rotas em config/routes.rb
rails g controller cria o controller, as views e adiciona as rotas. As actions listadas geram métodos vazios e templates correspondentes.
Gemfile
# Gemfile gem "rails", "~> 7.1" gem "sqlite3" gem "puma" group :development do gem "debug" end bundle install bundle add devise
O Gemfile lista as dependências. bundle install instala tudo. group :development limita gems a ambientes específicos. bundle add adiciona e instala numa só etapa.
Scaffold
rails g scaffold Post titulo:string texto:text # Gera tudo: model, controller, # views, migration, rotas, testes rails db:migrate
scaffold gera o CRUD completo de uma vez: model, controller com 7 actions, views, migration e rotas. Ideal para prototipagem rápida.
Ambientes
# config/environments/ # development.rb # test.rb # production.rb RAILS_ENV=production rails server Rails.env # => "development" Rails.env.production? # => false
Rails tem 3 ambientes: development, test e production. Cada um tem configs próprias em config/environments/. Rails.env retorna o ambiente activo.
Models e ActiveRecord
Definir model
class Post < ApplicationRecord validates :titulo, presence: true belongs_to :autor has_many :comentarios end
Todo model herda de ApplicationRecord. O nome da classe é singular (Post) e a tabela é plural (posts). validates e associações são declarados no corpo da classe.
Associações
has_many :comentarios belongs_to :post has_one :perfil has_many :tags, through: :taggings has_and_belongs_to_many :categorias
has_many / belongs_to são as mais comuns. through cria associação via tabela intermédia. has_and_belongs_to_many usa join table sem model próprio.
Eager loading
# N+1 problem:
Post.all.each { |p| p.autor.nome }
# Solução:
Post.includes(:autor).each { |p|
p.autor.nome }
Post.joins(:autor).where(
autors: { ativo: true })includes carrega associações em 2 queries (evita N+1). joins faz INNER JOIN — necessário para filtrar por campos da associação. preload força 2 queries separadas.
Validações
validates :email, presence: true,
uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :idade, numericality: {
greater_than: 0, only_integer: true }
validates :titulo, length: { maximum: 200 }validates aceita múltiplas regras: presence, uniqueness, format, numericality, length. Se falhar, save retorna false e errors é populado.
Scopes
scope :ativos, -> { where(ativo: true) }
scope :recentes, -> {
order(criado_em: :desc).limit(5) }
Post.ativos.recentesscope define queries nomeadas reutilizáveis com lambda. Podem ser encadeadas como métodos normais. Equivalentes a métodos de classe que retornam ActiveRecord::Relation.
CRUD básico
Post.create(titulo: "Olá", texto: "Mundo") Post.find(1) Post.find_by(titulo: "Olá") post.update(titulo: "Novo título") post.destroy
create instancia e guarda. find lança RecordNotFound se não existir; find_by retorna nil. update valida e guarda. destroy remove o registo.
Callbacks
class Post < ApplicationRecord
before_save :normalizar_titulo
after_create :notificar
private
def normalizar_titulo
self.titulo = titulo.strip.downcase
end
endCallbacks executam código em pontos do ciclo de vida: before_save, after_create, before_destroy. Úteis para normalização, logs e notificações automáticas.
Queries e condições
Post.where(ativo: true)
Post.where("views > ?", 100)
Post.order(criado_em: :desc)
Post.limit(10).offset(20)
Post.count
Post.pluck(:titulo)where aceita hash ou SQL com placeholders ?. As queries são encadeáveis (lazy). pluck retorna apenas os valores de uma coluna. count faz SELECT COUNT(*).
Enum
class Post < ApplicationRecord
enum :status, { rascunho: 0,
publicado: 1, arquivado: 2 }
end
post.publicado!
post.status # => "publicado"
Post.publicado # => todos publicadosenum mapeia inteiros para nomes legíveis. Gera métodos de query (Post.publicado) e de transição (post.publicado!). A coluna deve ser integer na BD.
Controllers
Actions RESTful
class PostsController < ApplicationController
def index
@posts = Post.all
end
def show
@post = Post.find(params[:id])
end
endAs 7 actions RESTful: index, show, new, create, edit, update, destroy. Variáveis com @ ficam disponíveis na view.
Flash e redirect
flash[:notice] = "Post criado!" flash[:alert] = "Erro ao guardar." redirect_to posts_path redirect_to @post, notice: "OK" redirect_to root_path, alert: "Negado"
flash guarda mensagens para o próximo request. notice (sucesso) e alert (erro) são atalhos inline no redirect_to. A mensagem desaparece após ser exibida.
Skip e herança
class ApplicationController < ActionController::Base before_action :autenticar! end class PublicController < ApplicationController skip_before_action :autenticar! end
ApplicationController é o pai de todos — filtros definidos aqui aplicam-se globalmente. skip_before_action remove um filtro herdado em controllers específicos.
Strong parameters
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: "Criado!"
else
render :new, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:titulo, :texto)
endparams.require(:post) exige a chave. permit lista os campos aceites — protege contra mass assignment. Sem isto, ActiveModel::ForbiddenAttributesError é lançado.
Render vs Redirect
# Render: mesma request, sem redirect render :new render partial: "form" render plain: "OK" # Redirect: nova request HTTP redirect_to posts_path
render mostra uma view sem mudar o URL (mesmo request). redirect_to envia HTTP 302 — o browser faz novo GET. Use render em erros de validação para manter dados do form.
Before actions
before_action :set_post, only: [:show, :edit, :update, :destroy] before_action :autenticar! private def set_post @post = Post.find(params[:id]) end
before_action executa antes das actions. only / except limitam onde corre. Ideal para carregar recursos e verificar autenticação sem repetir código.
Rescue e erros
rescue_from ActiveRecord::RecordNotFound,
with: :nao_encontrado
private
def nao_encontrado
render file: "public/404.html",
status: :not_found
endrescue_from captura excepções globalmente no controller. Evita repetir begin/rescue. Pode renderizar páginas de erro ou redireccionar com mensagens.
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 permite responder em múltiplos formatos conforme o header Accept. O bloco define a resposta para cada formato. A extensão da URL (.json) também activa.
Cookies e session
session[:user_id] = user.id session[:user_id] # => 42 session.delete(:user_id) cookies[:tema] = "escuro" cookies.permanent[:lembrar] = token
session guarda dados temporários (limpa ao fechar browser). cookies persiste no cliente. cookies.permanent expira em 20 anos. Ambos acessíveis via hash.
Routes
Resources
resources :posts # Gera 7 rotas: index, show, new, # create, edit, update, destroy resources :posts, only: [:index, :show] resources :posts, except: [:destroy]
resources gera as 7 rotas RESTful automaticamente. only / except limitam quais são criadas. Cada rota tem um helper nomeado (posts_path, post_path).
Namespace
namespace :admin do resources :users resources :posts end # /admin/users # Admin::UsersController
namespace agrupa rotas sob um prefixo e módulo. Cria controllers em app/controllers/admin/. Os helpers ganham prefixo: admin_users_path.
Rotas personalizadas
get "sobre", to: "pages#sobre" post "login", to: "sessions#create" root "posts#index" get "posts/:id/preview", to: "posts#preview"
get, post, put, delete definem rotas manuais. root define a página inicial. :id é um parâmetro dinâmico acessível via params[:id].
Scope e shallow
scope "/api", module: "api" do resources :posts end resources :posts, shallow: true do resources :comentarios end # /comentarios/5 (sem /posts/1)
scope personaliza prefixo/módulo sem criar namespace completo. shallow: true aninha apenas index/new/create — as restantes ficam no nível raiz.
Nested resources
resources :posts do resources :comentarios end # /posts/1/comentarios # /posts/1/comentarios/5
Recursos aninhados geram URLs hierárquicos. O controller recebe params[:post_id]. Limite a 1 nível de nesting para manter URLs legíveis.
Member e collection
resources :posts do
member do
get :preview
end
collection do
get :arquivados
end
end
# /posts/1/preview
# /posts/arquivadosmember adiciona rotas para um recurso específico (com :id). collection adiciona rotas para a colecção (sem :id). Geram helpers automáticos.
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
Cada rota gera helpers: _path (relativo) e _url (absoluto). Use _path em views e _url em emails e redirects externos.
Constraints
constraints subdomain: "api" do
resources :posts
end
get "posts/:id", to: "posts#show",
constraints: { id: /\d+/ }constraints restringe rotas por subdomínio, formato ou regex. Útil para APIs em subdomínios ou para validar parâmetros directamente na rota.
Views e ERB
ERB básico
<h1><%= @post.titulo %></h1> <p><%= @post.texto %></p> <%# comentário (não renderiza) %> <% if @post.ativo? %> <span>Publicado</span> <% end %>
<%= %> imprime o resultado (com escape HTML). <% %> executa sem imprimir. <%# %> é comentário. Variáveis @ do controller ficam disponíveis.
Layouts e yield
<!-- layouts/application.html.erb --> <body> <%= yield %> <%= yield :sidebar %> </body> <!-- Na view: --> <% content_for :sidebar do %> <nav>Menu lateral</nav> <% end %>
yield insere o conteúdo da view no layout. yield :sidebar insere conteúdo nomeado. content_for define blocos para slots específicos do layout.
Turbo e Stimulus
<%= turbo_frame_tag "posts" do %>
<%= render @posts %>
<% end %>
<%= link_to "Próxima", posts_path,
data: { turbo_frame: "posts" } %>Turbo Frames actualizam partes da página sem reload completo. turbo_frame_tag define uma região actualizável. Links com data-turbo-frame carregam dentro do frame.
Iteração
<% @posts.each do |post| %> <li><%= link_to post.titulo, post %></li> <% end %> <%= render @posts %>
each itera sobre colecções. render @posts renderiza automaticamente o partial _post.html.erb para cada item — mais limpo que loop manual.
Asset helpers
<%= image_tag "logo.png", alt: "Logo" %> <%= stylesheet_link_tag "app" %> <%= javascript_include_tag "app" %> <%= link_to "Ver post", post_path(@post) %>
image_tag, stylesheet_link_tag e javascript_include_tag geram tags com paths do asset pipeline. link_to cria links com helpers de rota.
Partials
<%= render "form" %> <%= render "posts/card", post: @post %> <!-- app/views/posts/_card.html.erb --> <div class="card"> <%= post.titulo %> </div>
Partials são ficheiros com prefixo _. render "form" procura _form.html.erb na mesma pasta. Passe variáveis locais como segundo argumento (hash).
Number e 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 formata valores monetários. time_ago_in_words retorna "há 3 dias". l() formata datas segundo o locale activo.
Form helpers
<%= form_with model: @post do |f| %> <%= f.label :titulo %> <%= f.text_field :titulo %> <%= f.text_area :texto, rows: 5 %> <%= f.select :status, ["rascunho", "publicado"] %> <%= f.submit "Guardar" %> <% end %>
form_with model: gera form para create ou update automaticamente. f.text_field, f.text_area, f.select criam inputs com nomes correctos. O CSRF token é incluído automaticamente.
Content tags e safe
<%= content_tag :div, "Olá", class: "msg" %> <%= tag.br %> <%= tag.input type: "text", name: "q" %> <%= raw @html %> <%= sanitize @html, tags: %w[b i em] %>
content_tag gera elementos HTML dinamicamente. tag.br cria tags self-closing. raw imprime sem escape (cuidado!). sanitize remove tags perigosas mantendo seguras.
Migrations
Criar migration
rails g migration CreatePosts \ titulo:string texto:text ativo:boolean rails g migration AddViewsToPosts \ views:integer
rails g migration gera ficheiro com timestamp. Nomes CreateX geram create_table. Nomes AddXToY geram add_column. O timestamp define a ordem de execução.
Change reversível
def change
create_table :posts do |t|
t.string :titulo, null: false
t.text :texto
t.timestamps
end
enddef change é auto-reversível — Rails sabe inverter create_table, add_column, etc. Use def up / def down apenas para operações irreversíveis.
Tipos de coluna
t.string :titulo t.text :corpo t.integer :views t.float :rating t.decimal :preco, precision: 8, scale: 2 t.boolean :ativo, default: false t.date :publicado_em t.datetime :created_at t.references :autor, foreign_key: true
Tipos mapeiam para SQL: string (varchar), text (sem limite), decimal (precisão exacta). references cria coluna autor_id com foreign key.
Timestamps e defaults
t.timestamps # Cria: created_at + updated_at t.boolean :ativo, default: true t.string :status, default: "rascunho" t.integer :views, default: 0
t.timestamps cria created_at e updated_at (actualizado automaticamente). default define valor inicial da coluna. Rails gere timestamps sem código extra.
Alterar esquema
add_column :posts, :views, :integer remove_column :posts, :views rename_column :posts, :old, :new add_index :posts, :titulo change_column_null :posts, :titulo, false
add_column / remove_column adicionam/removem colunas. add_index melhora performance de queries. change_column_null torna coluna obrigatória (NOT NULL).
Índices e constraints
add_index :posts, :titulo, unique: true add_index :posts, [:autor_id, :created_at] add_foreign_key :posts, :autors add_check_constraint :posts, "views >= 0", name: "views_positivos"
add_index com unique: true impede duplicados. Índices compostos (array) optimizam queries com múltiplas colunas. add_check_constraint valida a nível de BD.
Comandos de gestão
rails db:migrate rails db:rollback STEP=1 rails db:migrate:status rails db:reset rails db:schema:load
db:migrate aplica pendentes. db:rollback reverte a última (ou N com STEP). migrate:status mostra quais foram aplicadas. db:reset faz drop + create + migrate.
Seed de dados
# db/seeds.rb
Post.create!(titulo: "Primeiro",
texto: "Conteúdo inicial")
5.times do |i|
Post.create!(titulo: "Post #{i}")
end
# Executar:
rails db:seeddb/seeds.rb popula a BD com dados iniciais. create! (com bang) lança excepção se validação falhar. Execute com rails db:seed — seguro para idempotência.
Auth e Segurança
has_secure_password
gem "bcrypt"
class User < ApplicationRecord
has_secure_password
end
user.authenticate("senha123")has_secure_password adiciona hash bcrypt e método authenticate. Requer coluna password_digest. Retorna o user se a senha bater, false caso contrário.
Pundit (autorização)
gem "pundit"
class PostPolicy < ApplicationPolicy
def update?
user.admin? || record.autor == user
end
end
authorize @postPundit faz autorização com policies. Cada model tem uma classe Policy com métodos booleanos. authorize no controller verifica permissão — lança NotAuthorizedError se negado.
Devise
gem "devise" rails g devise:install rails g devise User rails db:migrate # Helpers disponíveis: current_user user_signed_in? authenticate_user!
Devise é a gem de auth mais popular. Gera registo, login, recuperação de senha e confirmação de email. authenticate_user! como before_action protege rotas.
SQL injection
# SEGURO (placeholder):
Post.where("titulo LIKE ?", "%#{q}%")
# PERIGOSO (nunca fazer):
Post.where("titulo LIKE '%#{q}%'")
# SEGURO (hash):
Post.where(titulo: params[:titulo])Use sempre placeholders ? ou hash em where. Interpolação directa permite SQL injection. O ActiveRecord faz escape automático com placeholders.
CSRF protection
# ApplicationController protect_from_forgery with: :exception # No layout (automático): <%= csrf_meta_tags %> # Em formulários: token incluído
Rails protege contra CSRF por defeito. csrf_meta_tags insere o token no HTML. Forms gerados com form_with incluem o token automaticamente. APIs usam skip_forgery_protection.
Headers de segurança
# config/application.rb
config.action_dispatch.default_headers = {
"X-Frame-Options" => "DENY",
"X-Content-Type-Options" => "nosniff",
"X-XSS-Protection" => "1; mode=block"
}Rails já inclui headers seguros por defeito. X-Frame-Options previne clickjacking. X-Content-Type-Options evita MIME sniffing. Personalize em default_headers.
XSS e sanitize
<%= @texto %> # escapa HTML <%= raw @html %> # NÃO escapa <%= sanitize @html, tags: %w[b i em p] %>
ERB escapa HTML automaticamente com <%= %>. raw desactiva o escape (perigoso). sanitize remove tags/scripts maliciosos mantendo uma whitelist segura.
Rate limiting
# config/initializers/rate_limit.rb
Rails.application.config.action_dispatch
.rate_limit = {
limit: 100,
period: 1.minute
}
# Ou manual com Rack::AttackRails 7.1+ tem rate limiting nativo. Limita requests por IP/período. Para controlo fino, use Rack::Attack (gem) com regras por endpoint, token ou IP.
API e Avançado
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 cria app sem views, assets ou cookies. Controllers herdam de ActionController::API (mais leve). render json: serializa objectos automaticamente.
Caching
# Fragment caching na view:
<% cache @post do %>
<%= render @post %>
<% end %>
# Low-level caching:
Rails.cache.fetch("populares", expires_in: 1.hour) do
Post.order(views: :desc).limit(10)
endcache na view guarda HTML renderizado (invalida ao actualizar). Rails.cache.fetch guarda qualquer dado com expiração. Use Redis como store em produção.
Serializers
gem "jsonapi-serializer"
class PostSerializer
include JSONAPI::Serializer
attributes :titulo, :texto
belongs_to :autor
attribute :url do |post|
post_url(post)
end
endjsonapi-serializer formata respostas no padrão JSON:API. attributes lista campos. attribute com bloco cria campos computados. Mais rápido que to_json manual.
Action Cable (WebSockets)
rails g channel Notificacoes
class NotificacoesChannel < ApplicationCable::Channel
def subscribed
stream_from "notificacoes"
end
end
ActionCable.server.broadcast(
"notificacoes", { msg: "Olá" })Action Cable integra WebSockets no Rails. stream_from subscreve um canal. broadcast envia mensagens a todos os subscritores. Ideal para notificações em tempo real.
Active Job
rails g job EnviarEmail
class EnviarEmailJob < ApplicationJob
queue_as :default
def perform(user)
UserMailer.bemvindo(user).deliver_now
end
end
EnviarEmailJob.perform_later(user)Active Job é a interface para background jobs. perform_later enfileira (não bloqueia). queue_as define a fila. Use Sidekiq ou Solid Queue como backend.
Engines e concerns
# app/models/concerns/pesquisavel.rb
module Pesquisavel
extend ActiveSupport::Concern
included do
scope :pesquisar, ->(q) {
where("titulo LIKE ?", "%#{q}%") }
end
end
class Post < ApplicationRecord
include Pesquisavel
endConcerns são módulos reutilizáveis (mixins). ActiveSupport::Concern permite hooks included. Mantêm models DRY. Engines são mini-apps empacotáveis (gems internas).
Action Mailer
rails g mailer UserMailer
def bemvindo(user)
@user = user
mail(to: user.email,
subject: "Bem-vindo!")
end
# app/views/user_mailer/bemvindo.html.erb
UserMailer.bemvindo(user).deliver_laterAction Mailer envia emails com templates ERB. deliver_later envia via background job. Configure SMTP em config/environments/production.rb ou use ActionMailer::Base.delivery_method.
Testes e Deploy
Minitest (unit)
class PostTest < ActiveSupport::TestCase
test "titulo é obrigatório" do
post = Post.new(titulo: nil)
assert_not post.valid?
assert_includes post.errors[:titulo],
"can't be blank"
end
endMinitest vem com Rails. test "..." define um caso. assert_not verifica falsidade. assert_includes confirma presença em array. Execute com rails test.
System tests
class PostsSystemTest < ApplicationSystemTestCase
test "criar post via browser" do
visit new_post_path
fill_in "Titulo", with: "Novo"
click_button "Guardar"
assert_text "Post criado!"
end
endSystem tests correm num browser real (Capybara). visit, fill_in, click_button simulam interacção do utilizador. Testam o fluxo completo (front + back).
Integration tests
class PostsFlowTest < ActionDispatch::IntegrationTest
test "criar post" do
post posts_path, params: {
post: { titulo: "Novo" } }
assert_response :redirect
follow_redirect!
assert_select "h1", "Novo"
end
endTestes de integração simulam requests HTTP completos. post posts_path envia request. assert_response verifica status. assert_select faz assert no HTML retornado.
Preparar produção
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 compila CSS/JS com fingerprint. force_ssl força HTTPS. rails secret gera secret_key_base. Logs em :info reduzem ruído.
Fixtures e factories
# test/fixtures/posts.yml
primeiro:
titulo: "Olá"
texto: "Mundo"
# Com FactoryBot (gem):
FactoryBot.define do
factory :post do
titulo { "Teste" }
end
endFixtures são dados YAML estáticos. FactoryBot gera dados dinâmicos com create(:post). Factories são mais flexíveis e evitam acoplamento entre testes.
Deploy (Capistrano)
gem "capistrano-rails" # config/deploy.rb set :application, "minha_app" set :repo_url, "git@github.com:user/app.git" set :deploy_to, "/var/www/app" cap production deploy
Capistrano automatiza deploy via SSH. Faz pull do git, bundle install, migrations e restart. Suporta rollback com cap production deploy:rollback.
RSpec (alternativa)
RSpec.describe Post do
it "requer titulo" do
post = Post.new(titulo: nil)
expect(post).not_to be_valid
end
it "cria com titulo" do
post = Post.create(titulo: "OK")
expect(post).to be_persisted
end
endRSpec é a alternativa mais popular ao Minitest. Sintaxe describe/it/expect. Matchers como be_valid, eq, include tornam testes legíveis.
Logs e debugging
Rails.logger.info("Processando...")
Rails.logger.error("Falha: #{e.message}")
# No controller/view:
debugger # pára execução (gem debug)
logger.debug params.inspectRails.logger escreve em log/development.log. debugger pára a execução para inspecção interactiva. params.inspect mostra todos os parâmetros recebidos.