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 mi_app cd mi_app rails server
gem install rails instala el framework globalmente. rails new genera la estructura completa del proyecto. rails server inicia Puma en localhost:3000.
Comandos CLI esenciales
rails console # IRB con la app cargada rails db:migrate # aplicar migrations rails db:seed # poblar BD rails routes # listar todas las rutas rails dbconsole # acceder al SQL directo
La rails console carga todo el entorno (models, configs). rails routes muestra verbos, paths y controllers mapeados. dbconsole abre el cliente SQL nativo.
Configuraciones
# config/application.rb config.time_zone = "Lisbon" config.i18n.default_locale = :es # config/credentials.yml.enc rails credentials:edit Rails.application.credentials.secret_api_key
config/application.rb define settings globales. credentials guarda secretos cifrados (API keys, passwords) — más seguro que variables de entorno en archivo.
Generar model
rails g model Post título:string texto:text rails g model User nombre:string email:string # Genera: # app/models/post.rb # db/migrate/xxxx_create_posts.rb
rails g model crea el archivo del model y la migration automáticamente. Los tipos (string, text, integer) definen las columnas de la tabla.
Estructura del proyecto
app/ models/ # ActiveRecord controllers/ # lógica HTTP views/ # templates ERB helpers/ # view helpers config/routes.rb # rutas db/migrate/ # migrations Gemfile # dependencias
Rails sigue el patrón MVC: models/ (datos), controllers/ (lógica), views/ (presentación). config/routes.rb mapea URLs a controllers.
Generar controller
rails g controller Posts index show new # Genera: # app/controllers/posts_controller.rb # app/views/posts/ (index, show, new) # rutas en config/routes.rb
rails g controller crea el controller, las views y añade las rutas. Las actions listadas generan métodos vacíos y templates correspondientes.
Gemfile
# Gemfile gem "rails", "~> 7.1" gem "sqlite3" gem "puma" group :development do gem "debug" end bundle install bundle add devise
El Gemfile lista las dependencias. bundle install instala todo. group :development limita gems a entornos específicos. bundle add añade e instala en un solo paso.
Scaffold
rails g scaffold Post título:string texto:text # Genera todo: model, controller, # views, migration, rutas, tests rails db:migrate
scaffold genera el CRUD completo de una vez: model, controller con 7 actions, views, migration y rutas. Ideal para prototipado rápido.
Entornos
# config/environments/ # development.rb # test.rb # production.rb RAILS_ENV=production rails server Rails.env # => "development" Rails.env.production? # => false
Rails tiene 3 entornos: development, test y production. Cada uno tiene configs propias en config/environments/. Rails.env devuelve el entorno activo.
Models e ActiveRecord
Definir model
class Post < ApplicationRecord validates :título, presence: true belongs_to :autor has_many :comentarios end
Todo model hereda de ApplicationRecord. El nombre de la clase es singular (Post) y la tabla es plural (posts). validates y asociaciones se declaran en el cuerpo de la clase.
Asociaciones
has_many :comentarios belongs_to :post has_one :perfil has_many :tags, through: :taggings has_and_belongs_to_many :categorias
has_many / belongs_to son las más comunes. through crea asociación vía tabla intermedia. has_and_belongs_to_many usa join table sin model propio.
Eager loading
# Problema N+1:
Post.all.each { |p| p.autor.nombre }
# Solución:
Post.includes(:autor).each { |p|
p.autor.nombre }
Post.joins(:autor).where(
autors: { activo: true })includes carga asociaciones en 2 queries (evita N+1). joins hace INNER JOIN — necesario para filtrar por campos de la asociación. preload fuerza 2 queries separadas.
Validaciones
validates :email, presence: true,
uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :edad, numericality: {
greater_than: 0, only_integer: true }
validates :título, length: { maximum: 200 }validates acepta múltiples reglas: presence, uniqueness, format, numericality, length. Si falla, save devuelve false y errors se llena.
Scopes
scope :activos, -> { where(activo: true) }
scope :recientes, -> {
order(creado_en: :desc).limit(5) }
Post.activos.recientesscope define queries con nombre reutilizables con lambda. Pueden encadenarse como métodos normales. Equivalentes a métodos de clase que devuelven ActiveRecord::Relation.
CRUD básico
Post.create(título: "Hola", texto: "Mundo") Post.find(1) Post.find_by(título: "Hola") post.update(título: "Nuevo título") post.destroy
create instancia y guarda. find lanza RecordNotFound si no existe; find_by devuelve nil. update valida y guarda. destroy elimina el registro.
Callbacks
class Post < ApplicationRecord
before_save :normalizar_título
after_create :notificar
private
def normalizar_título
self.título = título.strip.downcase
end
endCallbacks ejecutan código en puntos del ciclo de vida: before_save, after_create, before_destroy. Útiles para normalización, logs y notificaciones automáticas.
Queries y condiciones
Post.where(activo: true)
Post.where("views > ?", 100)
Post.order(creado_en: :desc)
Post.limit(10).offset(20)
Post.count
Post.pluck(:título)where acepta hash o SQL con placeholders ?. Las queries son encadenables (lazy). pluck devuelve solo los valores de una columna. count hace SELECT COUNT(*).
Enum
class Post < ApplicationRecord
enum :status, { borrador: 0,
publicado: 1, archivado: 2 }
end
post.publicado!
post.status # => "publicado"
Post.publicado # => todos los publicadosenum mapea enteros a nombres legibles. Genera métodos de query (Post.publicado) y de transición (post.publicado!). La columna debe ser integer en la BD.
Controllers
Actions RESTful
class PostsController < ApplicationController
def index
@posts = Post.all
end
def show
@post = Post.find(params[:id])
end
endLas 7 actions RESTful: index, show, new, create, edit, update, destroy. Las variables con @ quedan disponibles en la view.
Flash y redirect
flash[:notice] = "¡Post creado!" flash[:alert] = "Error al guardar." redirect_to posts_path redirect_to @post, notice: "OK" redirect_to root_path, alert: "Denegado"
flash guarda mensajes para el siguiente request. notice (éxito) y alert (error) son atajos inline en redirect_to. El mensaje desaparece tras mostrarse.
Skip y herencia
class ApplicationController < ActionController::Base before_action :autenticar! end class PublicController < ApplicationController skip_before_action :autenticar! end
ApplicationController es el padre de todos — los filtros definidos aquí se aplican globalmente. skip_before_action quita un filtro heredado en controllers específicos.
Strong parameters
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: "¡Creado!"
else
render :new, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:título, :texto)
endparams.require(:post) exige la clave. permit lista los campos aceptados — protege contra mass assignment. Sin esto, se lanza ActiveModel::ForbiddenAttributesError.
Render vs Redirect
# Render: misma request, sin redirect render :new render partial: "form" render plain: "OK" # Redirect: nueva request HTTP redirect_to posts_path
render muestra una view sin cambiar la URL (misma request). redirect_to envía HTTP 302 — el navegador hace un nuevo GET. Usa render en errores de validación para mantener los datos del 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 se ejecuta antes de las actions. only / except limitan dónde corre. Ideal para cargar recursos y verificar autenticación sin repetir código.
Rescue y errores
rescue_from ActiveRecord::RecordNotFound,
with: :no_encontrado
private
def no_encontrado
render file: "public/404.html",
status: :not_found
endrescue_from captura excepciones globalmente en el controller. Evita repetir begin/rescue. Puede renderizar páginas de error o redirigir con mensajes.
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 en múltiples formatos según el header Accept. El bloque define la respuesta para cada formato. La extensión de la URL (.json) también lo activa.
Cookies y session
session[:user_id] = user.id session[:user_id] # => 42 session.delete(:user_id) cookies[:tema] = "oscuro" cookies.permanent[:recordar] = token
session guarda datos temporales (se limpia al cerrar el navegador). cookies persiste en el cliente. cookies.permanent expira en 20 años. Ambos accesibles vía hash.
Routes
Resources
resources :posts # Genera 7 rutas: index, show, new, # create, edit, update, destroy resources :posts, only: [:index, :show] resources :posts, except: [:destroy]
resources genera las 7 rutas RESTful automáticamente. only / except limitan cuáles se crean. Cada ruta tiene un helper con nombre (posts_path, post_path).
Namespace
namespace :admin do resources :users resources :posts end # /admin/users # Admin::UsersController
namespace agrupa rutas bajo un prefijo y módulo. Crea controllers en app/controllers/admin/. Los helpers ganan prefijo: admin_users_path.
Rutas personalizadas
get "acerca", to: "pages#acerca" post "login", to: "sessions#create" root "posts#index" get "posts/:id/preview", to: "posts#preview"
get, post, put, delete definen rutas manuales. root define la página inicial. :id es un parámetro dinámico accesible vía params[:id].
Scope y shallow
scope "/api", module: "api" do resources :posts end resources :posts, shallow: true do resources :comentarios end # /comentarios/5 (sin /posts/1)
scope personaliza prefijo/módulo sin crear un namespace completo. shallow: true anida solo index/new/create — las restantes quedan en el nivel raíz.
Nested resources
resources :posts do resources :comentarios end # /posts/1/comentarios # /posts/1/comentarios/5
Los recursos anidados generan URLs jerárquicas. El controller recibe params[:post_id]. Limita el nesting a 1 nivel para mantener URLs legibles.
Member y collection
resources :posts do
member do
get :preview
end
collection do
get :archivados
end
end
# /posts/1/preview
# /posts/archivadosmember añade rutas para un recurso específico (con :id). collection añade rutas para la colección (sin :id). Generan 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 ruta genera helpers: _path (relativo) y _url (absoluto). Usa _path en views y _url en emails y redirects externos.
Constraints
constraints subdomain: "api" do
resources :posts
end
get "posts/:id", to: "posts#show",
constraints: { id: /\d+/ }constraints restringe rutas por subdominio, formato o regex. Útil para APIs en subdominios o para validar parámetros directamente en la ruta.
Views e ERB
ERB básico
<h1><%= @post.título %></h1> <p><%= @post.texto %></p> <%# comentario (no renderiza) %> <% if @post.activo? %> <span>Publicado</span> <% end %>
<%= %> imprime el resultado (con escape HTML). <% %> ejecuta sin imprimir. <%# %> es comentario. Las variables @ del controller quedan disponibles.
Layouts y yield
<!-- layouts/application.html.erb --> <body> <%= yield %> <%= yield :sidebar %> </body> <!-- En la view: --> <% content_for :sidebar do %> <nav>Menú lateral</nav> <% end %>
yield inserta el contenido de la view en el layout. yield :sidebar inserta contenido con nombre. content_for define bloques para slots específicos del layout.
Turbo y Stimulus
<%= turbo_frame_tag "posts" do %>
<%= render @posts %>
<% end %>
<%= link_to "Siguiente", posts_path,
data: { turbo_frame: "posts" } %>Turbo Frames actualizan partes de la página sin reload completo. turbo_frame_tag define una región actualizable. Los enlaces con data-turbo-frame cargan dentro del frame.
Iteración
<% @posts.each do |post| %> <li><%= link_to post.título, post %></li> <% end %> <%= render @posts %>
each itera sobre colecciones. render @posts renderiza automáticamente el partial _post.html.erb para cada ítem — más limpio que un 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 y javascript_include_tag generan tags con paths del asset pipeline. link_to crea enlaces con helpers de ruta.
Partials
<%= render "form" %> <%= render "posts/card", post: @post %> <!-- app/views/posts/_card.html.erb --> <div class="card"> <%= post.título %> </div>
Los partials son archivos con prefijo _. render "form" búsqueda _form.html.erb en la misma carpeta. Pasa variables locales como segundo argumento (hash).
Number y 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 formatea valores monetarios. time_ago_in_words devuelve "hace 3 días". l() formatea fechas según el locale activo.
Form helpers
<%= form_with model: @post do |f| %> <%= f.label :título %> <%= f.text_field :título %> <%= f.text_area :texto, rows: 5 %> <%= f.select :status, ["borrador", "publicado"] %> <%= f.submit "Guardar" %> <% end %>
form_with model: genera un form para create o update automáticamente. f.text_field, f.text_area, f.select crean inputs con los nombres correctos. El CSRF token se incluye automáticamente.
Content tags y safe
<%= content_tag :div, "Hola", class: "msg" %> <%= tag.br %> <%= tag.input type: "text", name: "q" %> <%= raw @html %> <%= sanitize @html, tags: %w[b i em] %>
content_tag genera elementos HTML dinámicamente. tag.br crea tags self-closing. raw imprime sin escape (¡cuidado!). sanitize elimina tags peligrosas manteniendo las seguras.
Migrations
Crear migration
rails g migration CreatePosts \ título:string texto:text activo:boolean rails g migration AddViewsToPosts \ views:integer
rails g migration genera un archivo con timestamp. Los nombres CreateX generan create_table. Los nombres AddXToY generan add_column. El timestamp define el orden de ejecución.
Change reversible
def change
create_table :posts do |t|
t.string :título, null: false
t.text :texto
t.timestamps
end
enddef change es auto-reversible — Rails sabe invertir create_table, add_column, etc. Usa def up / def down solo para operaciones irreversibles.
Tipos de columna
t.string :título t.text :cuerpo t.integer :views t.float :rating t.decimal :precio, precision: 8, scale: 2 t.boolean :activo, default: false t.date :publicado_en t.datetime :created_at t.references :autor, foreign_key: true
Los tipos mapean a SQL: string (varchar), text (sin límite), decimal (precisión exacta). references crea la columna autor_id con foreign key.
Timestamps y defaults
t.timestamps # Crea: created_at + updated_at t.boolean :activo, default: true t.string :status, default: "borrador" t.integer :views, default: 0
t.timestamps crea created_at y updated_at (actualizado automáticamente). default define el valor inicial de la columna. Rails gestiona los timestamps sin código extra.
Alterar esquema
add_column :posts, :views, :integer remove_column :posts, :views rename_column :posts, :old, :new add_index :posts, :título change_column_null :posts, :título, false
add_column / remove_column añaden/eliminan columnas. add_index mejora el rendimiento de las queries. change_column_null hace la columna obligatoria (NOT NULL).
Índices y constraints
add_index :posts, :título, unique: true add_index :posts, [:autor_id, :created_at] add_foreign_key :posts, :autors add_check_constraint :posts, "views >= 0", name: "views_positivas"
add_index con unique: true impide duplicados. Los índices compuestos (array) optimizan queries con múltiples columnas. add_check_constraint valida a nivel de BD.
Comandos de gestión
rails db:migrate rails db:rollback STEP=1 rails db:migrate:status rails db:reset rails db:schema:load
db:migrate aplica las pendientes. db:rollback revierte la última (o N con STEP). migrate:status muestra cuáles se aplicaron. db:reset hace drop + create + migrate.
Seed de datos
# db/seeds.rb
Post.create!(título: "Primero",
texto: "Contenido inicial")
5.times do |i|
Post.create!(título: "Post #{i}")
end
# Ejecutar:
rails db:seeddb/seeds.rb puebla la BD con datos iniciales. create! (con bang) lanza excepción si la validación falla. Ejecuta con rails db:seed — seguro para idempotencia.
Auth e Segurança
has_secure_password
gem "bcrypt"
class User < ApplicationRecord
has_secure_password
end
user.authenticate("clave123")has_secure_password añade hash bcrypt y el método authenticate. Requiere columna password_digest. Devuelve el user si la contraseña coincide, false en caso contrario.
Pundit (autorización)
gem "pundit"
class PostPolicy < ApplicationPolicy
def update?
user.admin? || record.autor == user
end
end
authorize @postPundit hace autorización con policies. Cada model tiene una clase Policy con métodos booleanos. authorize en el controller verifica el permiso — lanza NotAuthorizedError si se deniega.
Devise
gem "devise" rails g devise:install rails g devise User rails db:migrate # Helpers disponibles: current_user user_signed_in? authenticate_user!
Devise es la gem de auth más popular. Genera registro, login, recuperación de contraseña y confirmación de email. authenticate_user! como before_action protege rutas.
SQL injection
# SEGURO (placeholder):
Post.where("título LIKE ?", "%#{q}%")
# PELIGROSO (nunca hacer):
Post.where("título LIKE '%#{q}%'")
# SEGURO (hash):
Post.where(título: params[:título])Usa siempre placeholders ? o hash en where. La interpolación directa permite SQL injection. ActiveRecord hace escape automático con placeholders.
CSRF protection
# ApplicationController protect_from_forgery with: :exception # En el layout (automático): <%= csrf_meta_tags %> # En formularios: token incluido
Rails protege contra CSRF por defecto. csrf_meta_tags inserta el token en el HTML. Los forms generados con form_with incluyen el token automáticamente. Las APIs usan skip_forgery_protection.
Headers de seguridad
# config/application.rb
config.action_dispatch.default_headers = {
"X-Frame-Options" => "DENY",
"X-Content-Type-Options" => "nosniff",
"X-XSS-Protection" => "1; mode=block"
}Rails ya incluye headers seguros por defecto. X-Frame-Options previene clickjacking. X-Content-Type-Options evita MIME sniffing. Personaliza en default_headers.
XSS y sanitize
<%= @texto %> # escapa HTML <%= raw @html %> # NO escapa <%= sanitize @html, tags: %w[b i em p] %>
ERB escapa HTML automáticamente con <%= %>. raw desactiva el escape (peligroso). sanitize elimina tags/scripts maliciosos manteniendo una whitelist segura.
Rate limiting
# config/initializers/rate_limit.rb
Rails.application.config.action_dispatch
.rate_limit = {
limit: 100,
period: 1.minute
}
# O manual con Rack::AttackRails 7.1+ tiene rate limiting nativo. Limita requests por IP/período. Para control fino, usa Rack::Attack (gem) con reglas por endpoint, token o 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 crea una app sin views, assets ni cookies. Los controllers heredan de ActionController::API (más ligero). render json: serializa objetos automáticamente.
Caching
# Fragment caching en la 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 en la view guarda HTML renderizado (se invalida al actualizar). Rails.cache.fetch guarda cualquier dato con expiración. Usa Redis como store en producción.
Serializers
gem "jsonapi-serializer"
class PostSerializer
include JSONAPI::Serializer
attributes :título, :texto
belongs_to :autor
attribute :url do |post|
post_url(post)
end
endjsonapi-serializer formatea respuestas en el estándar JSON:API. attributes lista campos. attribute con bloque crea campos computados. Más rápido que to_json manual.
Action Cable (WebSockets)
rails g channel Notificaciones
class NotificacionesChannel < ApplicationCable::Channel
def subscribed
stream_from "notificaciones"
end
end
ActionCable.server.broadcast(
"notificaciones", { msg: "Hola" })Action Cable integra WebSockets en Rails. stream_from se suscribe a un canal. broadcast envía mensajes a todos los suscriptores. Ideal para notificaciones en tiempo real.
Active Job
rails g job EnviarEmail
class EnviarEmailJob < ApplicationJob
queue_as :default
def perform(user)
UserMailer.bienvenido(user).deliver_now
end
end
EnviarEmailJob.perform_later(user)Active Job es la interfaz para background jobs. perform_later encola (no bloquea). queue_as define la cola. Usa Sidekiq o Solid Queue como backend.
Engines y concerns
# app/models/concerns/buscable.rb
module Buscable
extend ActiveSupport::Concern
included do
scope :buscar, ->(q) {
where("título LIKE ?", "%#{q}%") }
end
end
class Post < ApplicationRecord
include Buscable
endConcerns son módulos reutilizables (mixins). ActiveSupport::Concern permite hooks included. Mantienen los models DRY. Engines son mini-apps empaquetables (gems internas).
Action Mailer
rails g mailer UserMailer
def bienvenido(user)
@user = user
mail(to: user.email,
subject: "¡Bienvenido!")
end
# app/views/user_mailer/bienvenido.html.erb
UserMailer.bienvenido(user).deliver_laterAction Mailer envía emails con templates ERB. deliver_later envía vía background job. Configura SMTP en config/environments/production.rb o usa ActionMailer::Base.delivery_method.
Testes e Deploy
Minitest (unit)
class PostTest < ActiveSupport::TestCase
test "título es obligatorio" do
post = Post.new(título: nil)
assert_not post.valid?
assert_includes post.errors[:título],
"can't be blank"
end
endMinitest viene con Rails. test "..." define un caso. assert_not verifica falsedad. assert_includes confirma presencia en un array. Ejecuta con rails test.
System tests
class PostsSystemTest < ApplicationSystemTestCase
test "crear post vía navegador" do
visit new_post_path
fill_in "Título", with: "Nuevo"
click_button "Guardar"
assert_text "¡Post creado!"
end
endLos system tests corren en un navegador real (Capybara). visit, fill_in, click_button simulan la interacción del usuario. Prueban el flujo completo (front + back).
Integration tests
class PostsFlowTest < ActionDispatch::IntegrationTest
test "crear post" do
post posts_path, params: {
post: { título: "Nuevo" } }
assert_response :redirect
follow_redirect!
assert_select "h1", "Nuevo"
end
endLos tests de integración simulan requests HTTP completos. post posts_path envía el request. assert_response verifica el status. assert_select hace assert sobre el HTML devuelto.
Preparar producción
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 con fingerprint. force_ssl fuerza HTTPS. rails secret genera secret_key_base. Logs en :info reducen ruido.
Fixtures y factories
# test/fixtures/posts.yml
primero:
título: "Hola"
texto: "Mundo"
# Con FactoryBot (gem):
FactoryBot.define do
factory :post do
título { "Prueba" }
end
endFixtures son datos YAML estáticos. FactoryBot genera datos dinámicos con create(:post). Las factories son más flexibles y evitan acoplamiento entre tests.
Deploy (Capistrano)
gem "capistrano-rails" # config/deploy.rb set :application, "mi_app" set :repo_url, "git@github.com:user/app.git" set :deploy_to, "/var/www/app" cap production deploy
Capistrano automatiza el deploy vía SSH. Hace pull del git, bundle install, migrations y restart. Soporta rollback con cap production deploy:rollback.
RSpec (alternativa)
RSpec.describe Post do
it "requiere título" do
post = Post.new(título: nil)
expect(post).not_to be_valid
end
it "crea con título" do
post = Post.create(título: "OK")
expect(post).to be_persisted
end
endRSpec es la alternativa más popular a Minitest. Sintaxis describe/it/expect. Matchers como be_valid, eq, include hacen los tests legibles.
Logs y debugging
Rails.logger.info("Procesando...")
Rails.logger.error("Fallo: #{e.message}")
# En el controller/view:
debugger # detiene la ejecución (gem debug)
logger.debug params.inspectRails.logger escribe en log/development.log. debugger detiene la ejecución para inspección interactiva. params.inspect muestra todos los parámetros recibidos.