
Rails Dev
- 233 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
rails-dev: A skill for development. This provides functionality for development workflows.
Key points
- rails-dev
Rails Dev by the numbers
- 233 all-time installs (skills.sh)
- +8 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,638 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill rails-devAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 233 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use rails-dev for development tasks?
Use rails-dev for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with rails-dev.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use rails-dev for development tasks, or when rails-dev: a skill for development. this provides functionality for development workflows.
What you get
Structured output aligned to rails-dev: rails-dev.
Files
Community Ruby on Rails Development Best Practices
Comprehensive performance and maintainability optimization guide for Ruby on Rails applications, maintained by Community. Contains 45 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
When to Apply
Reference these guidelines when:
- Writing new Rails controllers, models, or views
- Optimizing ActiveRecord queries and database access patterns
- Implementing caching strategies (fragment, Russian doll, low-level)
- Building or refactoring API endpoints
- Adding Turbo Frames and Streams for interactive UIs
- Reviewing code for N+1 queries and security vulnerabilities
- Designing background jobs with Sidekiq or Active Job
- Writing or reviewing database migrations
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Database & ActiveRecord | CRITICAL | db- |
| 2 | Controllers & Routing | CRITICAL | ctrl- |
| 3 | Security | HIGH | sec- |
| 4 | Models & Business Logic | HIGH | model- |
| 5 | Caching & Performance | HIGH | cache- |
| 6 | Views & Frontend | MEDIUM-HIGH | view- |
| 7 | API Design | MEDIUM | api- |
| 8 | Background Jobs & Async | LOW-MEDIUM | job- |
Quick Reference
1. Database & ActiveRecord (CRITICAL)
- `db-eager-load-associations` - Eager load associations to eliminate N+1 queries
- `db-add-database-indexes` - Add database indexes on queried columns
- `db-select-specific-columns` - Select only needed columns
- `db-batch-processing` - Use find_each for large dataset iteration
- `db-avoid-queries-in-loops` - Avoid database queries inside loops
- `db-use-scopes` - Define reusable query scopes on models
- `db-safe-migrations` - Write reversible zero-downtime migrations
- `db-exists-over-count` - Use exists? instead of count for existence checks
2. Controllers & Routing (CRITICAL)
- `ctrl-thin-controllers` - Keep controllers thin by delegating to models and services
- `ctrl-strong-params` - Always use strong parameters for mass assignment
- `ctrl-restful-routes` - Follow RESTful routing conventions
- `ctrl-before-action-scoping` - Scope before_action callbacks with only/except
- `ctrl-respond-to-format` - Use respond_to for multi-format responses
- `ctrl-rescue-from` - Handle errors with rescue_from in controllers
3. Security (HIGH)
- `sec-parameterized-queries` - Never interpolate user input in SQL
- `sec-strong-params-whitelist` - Whitelist permitted params, never blacklist
- `sec-authenticate-before-authorize` - Authenticate before authorize on every request
- `sec-csrf-protection` - Enable CSRF protection for all form submissions
- `sec-scope-queries-to-user` - Scope queries to current user for authorization
4. Models & Business Logic (HIGH)
- `model-validate-at-model-level` - Validate data at the model level
- `model-avoid-callback-side-effects` - Avoid side effects in model callbacks
- `model-use-service-objects` - Extract complex logic into service objects
- `model-scope-over-class-methods` - Use scopes instead of class methods for query composition
- `model-use-enums` - Use enums for finite state fields
- `model-concerns-for-shared-behavior` - Use concerns for shared model behavior
- `model-query-objects` - Extract complex queries into query objects
5. Caching & Performance (HIGH)
- `cache-fragment-caching` - Use fragment caching for expensive view partials
- `cache-russian-doll` - Use Russian doll caching for nested collections
- `cache-low-level` - Use Rails.cache.fetch for computed data
- `cache-counter-cache` - Use counter caches for association counts
- `cache-conditional-get` - Use conditional GET with stale? for HTTP caching
6. Views & Frontend (MEDIUM-HIGH)
- `view-collection-rendering` - Use collection rendering instead of loop partials
- `view-turbo-frames` - Use Turbo Frames for partial page updates
- `view-turbo-streams` - Use Turbo Streams for real-time page mutations
- `view-form-with` - Use form_with instead of form_tag or form_for
- `view-avoid-logic-in-views` - Move display logic to helpers or presenters
7. API Design (MEDIUM)
- `api-serializers` - Use serializers for consistent JSON responses
- `api-pagination` - Always paginate collection endpoints
- `api-versioning` - Version APIs from day one
- `api-error-responses` - Return structured error responses
- `api-avoid-jbuilder-hot-paths` - Avoid Jbuilder on high-traffic endpoints
8. Background Jobs & Async (LOW-MEDIUM)
- `job-idempotent-design` - Design jobs to be idempotent
- `job-small-payloads` - Pass IDs to jobs, not serialized objects
- `job-error-handling` - Configure retry and error handling for jobs
- `job-unique-jobs` - Prevent duplicate job enqueuing
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
Reference Files
| File | Description |
|---|---|
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for new rules |
| metadata.json | Version and reference information |
Rails Dev
This curated skill mirrors SKILL.md. When maintaining it, keep the guidance focused on Rails controllers, models, ActiveRecord, caching, security, background jobs, and Hotwire-backed app work.
Rule Title Here
Brief explanation of WHY this matters (1-3 sentences). Focus on performance or maintainability implications.
Incorrect (description of the problem/cost):
# Bad code example — production-realistic
class OrdersController < ApplicationController
def index
@orders = Order.all # Loads everything into memory
end
endCorrect (description of the benefit/solution):
# Good code example — minimal diff from incorrect
class OrdersController < ApplicationController
def index
@orders = Order.page(params[:page]).per(25)
end
endReference: Link to documentation
{
"version": "1.0.6",
"organization": "Community",
"technology": "Ruby on Rails",
"date": "February 2026",
"abstract": "Comprehensive performance and maintainability optimization guide for Ruby on Rails applications, designed for AI agents and LLMs. Contains 45 rules across 8 categories, prioritized by impact from critical (N+1 query elimination, thin controllers, database indexing) to incremental (idempotent job design, retry configuration). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.",
"references": [
"https://guides.rubyonrails.org",
"https://api.rubyonrails.org",
"https://edgeguides.rubyonrails.org",
"https://github.com/rubocop/rails-style-guide",
"https://turbo.hotwired.dev",
"https://github.com/flyerhzm/bullet",
"https://github.com/ankane/strong_migrations",
"https://github.com/kaminari/kaminari",
"https://guides.rubyonrails.org/caching_with_rails.html",
"https://guides.rubyonrails.org/security.html"
]
}
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Database & ActiveRecord (db)
Impact: CRITICAL Description: N+1 queries multiply latency by record count and are the #1 Rails performance killer. Proper eager loading, indexing, and query design eliminate the most costly bottleneck.
2. Controllers & Routing (ctrl)
Impact: CRITICAL Description: Fat controllers cascade into untestable, unmaintainable code. RESTful design, thin actions, and strong params enforce clean request handling across every endpoint.
3. Security (sec)
Impact: HIGH Description: SQL injection (CVSS 10.0), mass assignment, and IDOR are the top Rails vulnerabilities. Parameterized queries, strong params whitelisting, and scoped authorization prevent data breaches.
4. Models & Business Logic (model)
Impact: HIGH Description: Misused callbacks create hidden side effects that break in production. Service objects, scopes, and explicit validations keep domain logic predictable and testable.
5. Caching & Performance (cache)
Impact: HIGH Description: Fragment and Russian doll caching reduce database load by 10-100×. Counter caches and conditional GET eliminate redundant computation on every request.
6. Views & Frontend (view)
Impact: MEDIUM-HIGH Description: Partial render overhead compounds with collection size. Turbo Frames and Streams enable SPA-like interactivity without JavaScript framework complexity.
7. API Design (api)
Impact: MEDIUM Description: Over-fetching and missing pagination cause response bloat and client timeouts. Proper serialization and versioning keep APIs fast and forward-compatible.
8. Background Jobs & Async (job)
Impact: LOW-MEDIUM Description: Non-idempotent jobs cause duplicate processing on retry. Proper job design with small payloads and error handling ensures reliable async execution.
Avoid Jbuilder on High-Traffic Endpoints
Jbuilder creates a new template context per render, adding 2-5ms overhead per response. For high-traffic APIs, use plain Ruby serializers.
Incorrect (Jbuilder template):
# app/views/api/orders/index.json.jbuilder
json.orders @orders do |order|
json.id order.id
json.total order.total.to_f
json.status order.status
json.items order.items do |item|
json.id item.id
json.name item.product.name
end
endCorrect (plain Ruby serializer):
class Api::OrdersController < Api::BaseController
def index
orders = current_user.orders.includes(items: :product).page(params[:page])
render json: { orders: orders.map { |o| OrderSerializer.new(o).as_json } }
end
endWhen NOT to use this pattern:
- Low-traffic internal admin endpoints where development speed matters more
- Prototyping and MVPs
Reference: Jbuilder Gem
Return Structured Error Responses
Returning plain text errors or inconsistent JSON formats forces clients to parse strings. Use a consistent error envelope.
Incorrect (inconsistent error formats):
class Api::OrdersController < ApplicationController
def create
order = Order.new(order_params)
if order.save
render json: order
else
render json: order.errors.full_messages, status: :unprocessable_entity
end
end
def show
order = Order.find(params[:id])
render json: order
rescue ActiveRecord::RecordNotFound
render json: "Not found", status: :not_found # Plain string
end
endCorrect (consistent error envelope):
class Api::OrdersController < Api::BaseController
def create
order = Order.new(order_params)
if order.save
render json: OrderSerializer.new(order).as_json, status: :created
else
render json: {
error: "validation_failed",
message: "Order could not be created",
details: order.errors.messages
}, status: :unprocessable_entity
end
end
end
# app/controllers/api/base_controller.rb
class Api::BaseController < ApplicationController
rescue_from ActiveRecord::RecordNotFound do |e|
render json: {
error: "not_found",
message: "#{e.model} not found"
}, status: :not_found
end
endAlways Paginate Collection Endpoints
Returning unbounded collections causes memory spikes, slow responses, and client crashes on large datasets. Always paginate with cursor or offset-based pagination.
Incorrect (returns all records):
class Api::OrdersController < ApplicationController
def index
orders = current_user.orders # Returns ALL orders
render json: orders
end
endCorrect (paginated with metadata):
class Api::OrdersController < ApplicationController
def index
orders = current_user.orders
.order(created_at: :desc)
.page(params[:page])
.per(params[:per_page] || 25)
render json: {
orders: orders.map { |o| OrderSerializer.new(o).as_json },
meta: {
current_page: orders.current_page,
total_pages: orders.total_pages,
total_count: orders.total_count
}
}
end
endNote: Use cursor-based pagination for real-time feeds to avoid page drift:
orders = current_user.orders.where("id < ?", params[:cursor]).limit(25)Reference: Kaminari Gem
Use Serializers for Consistent JSON Responses
Building JSON inline with render json: and as_json scatters response structure across controllers. Serializers centralize the API contract.
Incorrect (ad-hoc JSON in controller):
class Api::OrdersController < ApplicationController
def show
order = Order.find(params[:id])
render json: {
id: order.id,
total: order.total.to_f,
status: order.status,
items: order.items.map { |item|
{ id: item.id, name: item.product.name, quantity: item.quantity }
},
customer: { id: order.user.id, name: order.user.name }
}
end
endCorrect (dedicated serializer):
# app/serializers/order_serializer.rb
class OrderSerializer
def initialize(order)
@order = order
end
def as_json
{
id: @order.id,
total: @order.total.to_f,
status: @order.status,
items: @order.items.map { |item| ItemSerializer.new(item).as_json },
customer: UserSerializer.new(@order.user).as_json
}
end
end
# Controller
class Api::OrdersController < ApplicationController
def show
order = Order.includes(:items, :user).find(params[:id])
render json: OrderSerializer.new(order).as_json
end
endReference: Rendering JSON — Rails Guides
Version APIs from Day One
Unversioned APIs force all clients to update simultaneously on breaking changes. Namespace APIs with version prefixes from the start.
Incorrect (unversioned API):
# config/routes.rb
namespace :api do
resources :orders
resources :users
endCorrect (versioned from day one):
# config/routes.rb
namespace :api do
namespace :v1 do
resources :orders
resources :users
end
end
# app/controllers/api/v1/orders_controller.rb
class Api::V1::OrdersController < Api::BaseController
def index
orders = current_user.orders.page(params[:page])
render json: orders.map { |o| OrderSerializer.new(o).as_json }
end
endBenefits:
- Old clients continue working on v1
- New features ship in v2 without breaking v1
- Deprecation timeline per version
Reference: Rails Routing — Rails Guides
Use Conditional GET with stale? for HTTP Caching
Rendering a response the client already has wastes server resources. stale? sends a 304 Not Modified when the resource hasn't changed, skipping view rendering entirely.
Incorrect (always renders full response):
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
end
endCorrect (conditional GET):
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
if stale?(@article)
respond_to do |format|
format.html
format.json { render json: @article }
end
end
end
endAlternative (for collection endpoints):
def index
@articles = Article.published.order(updated_at: :desc)
if stale?(etag: @articles, last_modified: @articles.maximum(:updated_at))
respond_to do |format|
format.html
format.json { render json: @articles }
end
end
endReference: Caching with Rails — Rails Guides
Use Counter Caches for Association Counts
Calling .count on associations triggers a COUNT query every time. Counter caches store the count in a column that updates automatically on create/destroy.
Incorrect (COUNT query on every render):
# app/models/post.rb
class Post < ApplicationRecord
has_many :comments
end
# In view — fires COUNT(*) query per post
<% @posts.each do |post| %>
<span><%= post.comments.count %> comments</span>
<% end %>Correct (counter cache column):
# Migration
class AddCommentsCountToPosts < ActiveRecord::Migration[7.1]
def change
add_column :posts, :comments_count, :integer, default: 0, null: false
end
end
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
end
# In view — reads column, zero queries
<% @posts.each do |post| %>
<span><%= post.comments_count %> comments</span>
<% end %>Important: Backfill counters for existing data in a separate data migration or rake task. Without this, all existing records show comments_count: 0:
# Run after deploying the schema migration
Post.find_each { |post| Post.reset_counters(post.id, :comments) }Reference: Active Record Associations — Rails Guides
Use Fragment Caching for Expensive View Partials
Re-rendering complex partials on every request wastes CPU. Fragment caching stores rendered HTML and serves it directly on subsequent requests.
Incorrect (re-renders on every request):
<!-- app/views/projects/show.html.erb -->
<div class="project-stats">
<h2><%= @project.name %> Statistics</h2>
<p>Total tasks: <%= @project.tasks.count %></p>
<p>Completed: <%= @project.tasks.completed.count %></p>
<p>Contributors: <%= @project.members.count %></p>
<%= render partial: "activity_feed", collection: @project.recent_activities %>
</div>Correct (cached fragment):
<!-- app/views/projects/show.html.erb -->
<% cache @project do %>
<div class="project-stats">
<h2><%= @project.name %> Statistics</h2>
<p>Total tasks: <%= @project.tasks.count %></p>
<p>Completed: <%= @project.tasks.completed.count %></p>
<p>Contributors: <%= @project.members.count %></p>
<%= render partial: "activity_feed", collection: @project.recent_activities %>
</div>
<% end %>Benefits:
- Cache key auto-expires when
@project.updated_atchanges - Zero code changes needed for cache invalidation with touch
Reference: Caching with Rails — Rails Guides
Use Rails.cache.fetch for Computed Data
Recomputing expensive aggregations on every request wastes CPU and database resources. Use Rails.cache.fetch with an expiry to cache results.
Incorrect (recomputes on every request):
class DashboardController < ApplicationController
def show
@total_revenue = Order.completed.sum(:total) # Full table scan every time
@top_products = Product.top_sellers(limit: 10) # Expensive aggregation
@user_growth = User.monthly_growth_rate # Reads entire users table
end
endCorrect (cached with expiry):
class DashboardController < ApplicationController
def show
@total_revenue = Rails.cache.fetch("dashboard/revenue", expires_in: 15.minutes) do
Order.completed.sum(:total)
end
@top_products = Rails.cache.fetch("dashboard/top_products", expires_in: 1.hour) do
Product.top_sellers(limit: 10).to_a
end
@user_growth = Rails.cache.fetch("dashboard/user_growth", expires_in: 1.day) do
User.monthly_growth_rate
end
end
endBenefits:
- Automatic cache miss handling (computes and stores on first call)
- Configurable TTL per data freshness requirements
- Works with any cache store (Redis, Memcached, memory)
Reference: Caching with Rails — Rails Guides
Use Russian Doll Caching for Nested Collections
When a child record updates, only its fragment re-renders. The outer fragment reuses all other cached children. Use touch: true on associations to propagate cache invalidation.
Incorrect (flat caching, entire list re-renders on any change):
<!-- Re-renders ALL comments when one changes -->
<% cache @post do %>
<h1><%= @post.title %></h1>
<% @post.comments.each do |comment| %>
<div class="comment">
<p><%= comment.body %></p>
<span><%= comment.author.name %></span>
</div>
<% end %>
<% end %>Correct (nested cache fragments):
<% cache @post do %>
<h1><%= @post.title %></h1>
<% @post.comments.each do |comment| %>
<% cache comment do %>
<div class="comment">
<p><%= comment.body %></p>
<span><%= comment.author.name %></span>
</div>
<% end %>
<% end %>
<% end %># app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post, touch: true # Invalidates post cache when comment changes
endReference: Caching with Rails — Rails Guides
Scope before_action Callbacks with only/except
Unscoped before_action runs on every action in the controller, including actions that don't need it. This causes unnecessary database queries and unexpected authorization failures.
Incorrect (runs on every action):
class ProjectsController < ApplicationController
before_action :authenticate_user!
before_action :set_project
before_action :authorize_admin
def index; end
def show; end
def edit; end
def update; end
endCorrect (scoped to relevant actions):
class ProjectsController < ApplicationController
before_action :authenticate_user!
before_action :set_project, only: [:show, :edit, :update]
before_action :authorize_admin, only: [:edit, :update]
def index; end
def show; end
def edit; end
def update; end
private
def set_project
@project = Project.find(params[:id])
end
endReference: Action Controller Overview — Rails Guides
Handle Errors with rescue_from in Controllers
Scattering begin/rescue blocks in individual actions creates inconsistent error responses. Use rescue_from to handle errors uniformly.
Incorrect (rescue in every action):
class OrdersController < ApplicationController
def show
@order = Order.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to orders_path, alert: "Order not found"
end
def update
@order = Order.find(params[:id])
@order.update!(order_params)
rescue ActiveRecord::RecordNotFound
redirect_to orders_path, alert: "Order not found"
rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.message
render :edit, status: :unprocessable_entity
end
endCorrect (centralized error handling):
class OrdersController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
rescue_from ActiveRecord::RecordInvalid, with: :record_invalid
def show
@order = Order.find(params[:id])
end
def update
@order = Order.find(params[:id])
@order.update!(order_params)
redirect_to @order
end
private
def record_not_found
redirect_to orders_path, alert: "Order not found"
end
def record_invalid(exception)
flash.now[:alert] = exception.message
render :edit, status: :unprocessable_entity
end
endReference: Action Controller Overview — Rails Guides
Use respond_to for Multi-Format Responses
Separate endpoints for HTML and JSON responses duplicate query logic. Use respond_to to serve multiple formats from one action.
Incorrect (duplicate controllers for HTML and JSON):
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def index
@orders = current_user.orders.recent
end
end
# app/controllers/api/orders_controller.rb
class Api::OrdersController < ApplicationController
def index
orders = current_user.orders.recent # Duplicated query
render json: orders
end
endCorrect (single action, multiple formats):
class OrdersController < ApplicationController
def index
@orders = current_user.orders.includes(:items).recent
respond_to do |format|
format.html
format.json { render json: @orders }
format.csv { send_data @orders.to_csv, filename: "orders.csv" }
end
end
endWhen NOT to use this pattern:
- For external APIs — use dedicated
Api::V1::controllers with versioning and serializers - When HTML and JSON responses need significantly different query logic or authorization
Reference: Layouts and Rendering — Rails Guides
Follow RESTful Routing Conventions
Custom routes outside REST conventions create inconsistent APIs and force developers to learn bespoke URL patterns. Map actions to standard CRUD operations.
Incorrect (custom non-RESTful routes):
# config/routes.rb
get "/orders/search", to: "orders#search"
post "/orders/mark_shipped", to: "orders#mark_shipped"
get "/orders/export_csv", to: "orders#export_csv"
post "/orders/bulk_delete", to: "orders#bulk_delete"Correct (RESTful resources with member/collection routes):
# config/routes.rb
resources :orders do
collection do
get :search
end
member do
patch :ship
end
end
resources :orders, only: [] do
resource :export, only: :show, module: :orders
resource :bulk_deletion, only: :create, module: :orders
endBenefits:
- Predictable URL patterns for every resource
- Separate controllers for distinct responsibilities
- Standard HTTP verbs map to standard actions
Reference: Rails Routing — Rails Guides
Always Use Strong Parameters for Mass Assignment
Passing raw params to model methods allows attackers to set any attribute, including admin, role, or password. Always whitelist permitted attributes.
Incorrect (permits all params):
class UsersController < ApplicationController
def update
@user = User.find(params[:id])
@user.update(params[:user].permit!) # Permits EVERYTHING including role, admin
end
endCorrect (explicit whitelist):
class UsersController < ApplicationController
def update
@user = User.find(params[:id])
@user.update(user_params)
end
private
def user_params
params.require(:user).permit(:name, :email, :avatar)
end
endAlternative (nested attributes):
def order_params
params.require(:order).permit(
:shipping_address,
items_attributes: [:product_id, :quantity]
)
endReference: Action Controller Overview — Rails Guides
Keep Controllers Thin by Delegating to Models and Services
Controllers that contain business logic become untestable and unmaintainable. Delegate domain logic to models or service objects, keeping actions to 5-10 lines.
Incorrect (business logic in controller):
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
@order.total = calculate_total(order_params[:items])
@order.tax = @order.total * tax_rate_for(@order.shipping_address)
@order.discount = apply_promo_code(params[:promo_code], @order.total)
@order.final_total = @order.total + @order.tax - @order.discount
if @order.save
OrderMailer.confirmation(@order).deliver_later
InventoryService.reserve_items(@order.items)
redirect_to @order
else
render :new, status: :unprocessable_entity
end
end
endCorrect (delegates to service object):
class OrdersController < ApplicationController
def create
result = Orders::PlaceOrder.call(
params: order_params,
promo_code: params[:promo_code],
user: current_user
)
if result.success?
redirect_to result.order
else
@order = result.order
render :new, status: :unprocessable_entity
end
end
endBenefits:
- Controller actions stay under 10 lines
- Business logic is testable without HTTP context
- Service objects are reusable across controllers and jobs
Reference: Rails Controller Patterns — AppSignal Blog
Add Database Indexes on Queried Columns
Every column used in WHERE, JOIN, or ORDER BY needs an index. t.references adds a foreign key index by default since Rails 5, but columns like status and placed_at used in queries get no automatic index.
Incorrect (no indexes on frequently queried columns):
class CreateOrders < ActiveRecord::Migration[7.1]
def change
create_table :orders do |t|
t.references :user, foreign_key: true # Index on user_id added by default
t.string :status # No index — full table scan
t.datetime :placed_at # No index — full table scan
t.timestamps
end
end
end
# These queries hit full table scans on status and placed_at
Order.where(status: "pending", user_id: current_user.id)
Order.where(user_id: current_user.id).order(placed_at: :desc)Correct (indexed columns for common query patterns):
class CreateOrders < ActiveRecord::Migration[7.1]
def change
create_table :orders do |t|
t.references :user, foreign_key: true
t.string :status
t.datetime :placed_at
t.timestamps
end
add_index :orders, :status
add_index :orders, [:user_id, :status] # Composite for combined lookups
add_index :orders, :placed_at
end
endBenefits:
- Composite indexes cover multiple query patterns in a single index
- Place the most selective column first in composite indexes
Reference: Active Record Migrations — Rails Guides
Avoid Database Queries Inside Loops
Executing queries inside loops creates N+1 patterns even without associations. Collect IDs first, then query once.
Incorrect (1 query per iteration):
order_ids = [1, 5, 23, 42, 99]
order_ids.each do |id|
order = Order.find(id) # 5 separate SELECT queries
process_order(order)
endCorrect (single query with where):
order_ids = [1, 5, 23, 42, 99]
orders = Order.where(id: order_ids).index_by(&:id)
order_ids.each do |id|
process_order(orders[id])
endReference: Active Record Query Interface — Rails Guides
Use find_each for Large Dataset Iteration
Loading thousands of records with .all.each loads every record into memory at once. Use find_each to process records in batches of 1,000.
Incorrect (loads all records into memory):
User.where(active: true).each do |user|
UserMailer.weekly_digest(user).deliver_later # 100k User objects in memory
endCorrect (processes in batches of 1,000):
User.where(active: true).find_each do |user|
UserMailer.weekly_digest(user).deliver_later
endAlternative (custom batch size and access to batch):
User.where(active: true).find_in_batches(batch_size: 500) do |batch|
UserMailer.bulk_digest(batch.map(&:id)).deliver_later
endReference: Active Record Query Interface — Rails Guides
Eager Load Associations to Eliminate N+1 Queries
Traversing associations without eager loading triggers one query per record. Use includes to load associations in 1-2 queries regardless of collection size.
Incorrect (N+1 queries, 101 queries for 100 posts):
posts = Post.all
posts.each do |post|
puts post.author.name # Fires a SELECT for each post
endCorrect (2 queries total):
posts = Post.includes(:author).all
posts.each do |post|
puts post.author.name
endAlternative (use `preload` when you need separate queries):
posts = Post.preload(:author, :comments).allAlternative (use `eager_load` when filtering on association):
posts = Post.eager_load(:author).where(authors: { active: true })When NOT to use this pattern:
- Single record lookups where you access only one association
- When you explicitly need lazy loading for conditional access
Reference: Active Record Query Interface — Rails Guides
Use exists? Instead of count for Existence Checks
count > 0 forces the database to count every matching row. exists? stops at the first match and returns immediately.
Incorrect (counts all matching rows):
if Order.where(user_id: current_user.id, status: "pending").count > 0
redirect_to checkout_path
endCorrect (stops at first match):
if Order.where(user_id: current_user.id, status: "pending").exists?
redirect_to checkout_path
endAlternative (use `any?` for loaded relations):
if current_user.orders.loaded? && current_user.orders.any?(&:pending?)
redirect_to checkout_path
endReference: Active Record Query Interface — Rails Guides
Write Reversible Zero-Downtime Migrations
Irreversible migrations block rollbacks. Long-running migrations lock tables and cause downtime. Use reversible patterns and avoid locking operations.
Incorrect (irreversible and locks table):
class UpdateUsersTable < ActiveRecord::Migration[7.1]
def change
remove_column :users, :legacy_role # Irreversible without type info
rename_column :users, :name, :full_name # Locks table during rename
end
endCorrect (reversible with safety):
class UpdateUsersTable < ActiveRecord::Migration[7.1]
def change
remove_column :users, :legacy_role, :string, default: "member" # Reversible
safety_assured do # strong_migrations gem
rename_column :users, :name, :full_name
end
end
endBenefits:
- Including column type makes
remove_columnreversible strong_migrationsgem catches unsafe operations before deploy
Reference: Active Record Migrations — Rails Guides
Select Only Needed Columns
Loading all columns wastes memory and bandwidth, especially on tables with text/blob columns. Use select or pluck to fetch only what you need.
Incorrect (loads all 20+ columns per row):
users = User.where(active: true)
user_emails = users.map(&:email) # Loads entire User objects into memoryCorrect (loads only needed column):
user_emails = User.where(active: true).pluck(:email) # Returns array of stringsAlternative (when you still need AR objects):
users = User.where(active: true).select(:id, :email, :name)When NOT to use this pattern:
- When you need the full object for updates or serialization
- When table has fewer than 5 columns
Reference: Active Record Query Interface — Rails Guides
Define Reusable Query Scopes on Models
Repeating query conditions across controllers leads to inconsistency and duplication. Scopes encapsulate query logic in the model and compose cleanly.
Incorrect (duplicated query logic in controllers):
# OrdersController
orders = Order.where(status: "pending").where("placed_at > ?", 30.days.ago)
# ReportsController
orders = Order.where(status: "pending").where("placed_at > ?", 30.days.ago)Correct (reusable scopes on model):
# app/models/order.rb
class Order < ApplicationRecord
scope :pending, -> { where(status: "pending") }
scope :recent, -> { where("placed_at > ?", 30.days.ago) }
end
# Any controller
orders = Order.pending.recentBenefits:
- Scopes are chainable and composable
- Single source of truth for query logic
- Testable in isolation
Reference: Active Record Query Interface — Rails Guides
Configure Retry and Error Handling for Jobs
Default retry behavior retries indefinitely on all errors. Configure explicit retry limits, backoff, and discard rules for known error types.
Incorrect (default unlimited retries):
class ImportOrdersJob < ApplicationJob
def perform(csv_url)
data = HTTP.get(csv_url) # Retries forever if URL is permanently broken
OrderImporter.import(data.body)
end
endCorrect (explicit retry configuration):
class ImportOrdersJob < ApplicationJob
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
discard_on ActiveRecord::RecordNotFound
def perform(csv_url)
data = HTTP.get(csv_url)
OrderImporter.import(data.body)
end
endBenefits:
retry_onwithattemptsprevents infinite loopsdiscard_onskips permanently unprocessable jobs- Polynomial backoff prevents thundering herd on recovery
Reference: Active Job Basics — Rails Guides
Design Jobs to Be Idempotent
Jobs will be retried on failure. If a job charges a credit card or sends an email without idempotency checks, retries cause duplicate charges or emails.
Incorrect (non-idempotent, duplicates on retry):
class ChargeOrderJob < ApplicationJob
def perform(order_id)
order = Order.find(order_id)
PaymentGateway.charge(order.user.payment_method, order.total) # Charges again on retry
OrderMailer.receipt(order).deliver_now # Sends duplicate email on retry
end
endCorrect (idempotent with guard checks):
class ChargeOrderJob < ApplicationJob
def perform(order_id)
order = Order.find(order_id)
return if order.charged?
PaymentGateway.charge(order.user.payment_method, order.total)
order.update!(charged_at: Time.current)
OrderMailer.receipt(order).deliver_now
end
endBenefits:
- Safe to retry any number of times
- Database state tracks completion
- No duplicate side effects
Reference: Active Job Basics — Rails Guides
Pass IDs to Jobs, Not Serialized Objects
ActiveRecord objects serialized via GlobalID are re-fetched by ID when the job executes. If the record is deleted between enqueue and execution, Rails raises ActiveJob::DeserializationError. Pass plain IDs for resilient job design.
Incorrect (raises DeserializationError if record deleted):
class SendReceiptJob < ApplicationJob
def perform(order)
# If order is deleted before job runs, raises DeserializationError
OrderMailer.receipt(order).deliver_now
end
end
# Enqueue
SendReceiptJob.perform_later(Order.find(42))Correct (handles missing records gracefully):
class SendReceiptJob < ApplicationJob
def perform(order_id)
order = Order.find_by(id: order_id)
return unless order # Gracefully skip if deleted
OrderMailer.receipt(order).deliver_now
end
end
# Enqueue
SendReceiptJob.perform_later(42)When NOT to use this pattern:
- When you want ActiveJob to automatically retry on DeserializationError (GlobalID is fine)
- For simple jobs where the record will always exist at execution time
Reference: Active Job Basics — Rails Guides
Prevent Duplicate Job Enqueuing
Rapid user actions or webhook retries enqueue the same job multiple times. Use unique job locks to deduplicate.
Incorrect (duplicate jobs enqueued):
class SyncInventoryJob < ApplicationJob
def perform(product_id)
product = Product.find(product_id)
InventoryService.sync(product) # 10 webhook hits = 10 identical syncs
end
end
# Webhook handler
product.webhooks.each do |webhook|
SyncInventoryJob.perform_later(product.id) # Enqueues multiple times
endCorrect (unique job with lock):
# Using SolidQueue (Rails 8+) or sidekiq-unique-jobs
class SyncInventoryJob < ApplicationJob
self.queue_adapter = :solid_queue
limits_concurrency to: 1, key: ->(product_id) { "sync_inventory_#{product_id}" }
def perform(product_id)
product = Product.find(product_id)
InventoryService.sync(product)
end
endAlternative (manual deduplication):
class SyncInventoryJob < ApplicationJob
def perform(product_id)
lock_key = "sync_inventory_#{product_id}"
return if Rails.cache.exist?(lock_key)
Rails.cache.write(lock_key, true, expires_in: 5.minutes)
product = Product.find(product_id)
InventoryService.sync(product)
end
endReference: Active Job Basics — Rails Guides
Avoid Side Effects in Model Callbacks
Callbacks that send emails, call APIs, or enqueue jobs run on every save — including seeds, tests, and console updates. Extract side effects into explicit service calls.
Incorrect (side effects in callback):
class Order < ApplicationRecord
after_create :send_confirmation_email
after_create :reserve_inventory
after_create :notify_warehouse
private
def send_confirmation_email
OrderMailer.confirmation(self).deliver_later # Fires on seeds and tests
end
def reserve_inventory
InventoryService.reserve(line_items) # Fires on console creates
end
def notify_warehouse
WarehouseApi.notify(self) # External API on every create
end
endCorrect (explicit service call):
class Order < ApplicationRecord
# Only data-integrity callbacks in model
before_validation :normalize_status
end
# app/services/orders/place_order.rb
class Orders::PlaceOrder
def self.call(order:)
return false unless order.save
OrderMailer.confirmation(order).deliver_later
InventoryService.reserve(order.line_items)
WarehouseApi.notify(order)
true
end
endWhen NOT to use this pattern:
- Data normalization callbacks (before_validation) are fine
- Touching parent timestamps (after_save :touch) is appropriate
- Counter cache updates belong in callbacks
Reference: Active Record Callbacks — Rails Guides
Use Concerns for Shared Model Behavior
Duplicating validations, scopes, and callbacks across models violates DRY. Concerns extract shared behavior into reusable modules.
Incorrect (duplicated logic across models):
class Post < ApplicationRecord
scope :published, -> { where("published_at <= ?", Time.current) }
scope :draft, -> { where(published_at: nil) }
def published?
published_at.present? && published_at <= Time.current
end
end
class Page < ApplicationRecord
scope :published, -> { where("published_at <= ?", Time.current) } # Duplicated
scope :draft, -> { where(published_at: nil) } # Duplicated
def published? # Duplicated
published_at.present? && published_at <= Time.current
end
endCorrect (shared concern):
# app/models/concerns/publishable.rb
module Publishable
extend ActiveSupport::Concern
included do
scope :published, -> { where("published_at <= ?", Time.current) }
scope :draft, -> { where(published_at: nil) }
end
def published?
published_at.present? && published_at <= Time.current
end
end
# app/models/post.rb
class Post < ApplicationRecord
include Publishable
end
# app/models/page.rb
class Page < ApplicationRecord
include Publishable
endExtract Complex Queries into Query Objects
Multi-join queries with conditional logic bloat models and resist testing. Query objects encapsulate complex queries in dedicated classes.
Incorrect (complex query in model):
class Order < ApplicationRecord
def self.dashboard_summary(user, start_date, end_date)
joins(:items, :payments)
.where(user_id: user.id)
.where(placed_at: start_date..end_date)
.where(payments: { status: "completed" })
.group(:status)
.select("orders.status, COUNT(*) as order_count, SUM(payments.amount) as total_revenue")
.having("SUM(payments.amount) > ?", 0)
end
endCorrect (query object):
# app/queries/order_dashboard_query.rb
class OrderDashboardQuery
def initialize(user:, start_date:, end_date:)
@user = user
@start_date = start_date
@end_date = end_date
end
def call
Order
.joins(:items, :payments)
.where(user_id: @user.id)
.where(placed_at: @start_date..@end_date)
.where(payments: { status: "completed" })
.group(:status)
.select("orders.status, COUNT(*) as order_count, SUM(payments.amount) as total_revenue")
.having("SUM(payments.amount) > ?", 0)
end
end
# Usage
OrderDashboardQuery.new(user: current_user, start_date: 30.days.ago, end_date: Time.current).callBenefits:
- Testable with focused specs
- Reusable across controllers and reports
- Keeps model file under 200 lines
Use Scopes Instead of Class Methods for Query Composition
Class methods with conditional logic can return nil, breaking query chains. Scopes automatically wrap nil returns in .all, making them safe for conditional logic and always chainable.
Incorrect (class method with conditional returns nil):
class Article < ApplicationRecord
def self.published
where(published: true) if publishing_enabled? # Returns nil when condition is false
end
def self.featured
where(featured: true)
end
end
# Breaks when published returns nil
Article.published.featured # NoMethodError: undefined method 'featured' for nilCorrect (scope wraps nil in .all automatically):
class Article < ApplicationRecord
scope :published, -> { where(published: true) if publishing_enabled? } # Returns .all when nil
scope :featured, -> { where(featured: true) }
scope :recent, -> { order(created_at: :desc) }
end
# Always chainable — scope returns .all when block returns nil
Article.published.featured.recentNote: Class methods that always return a relation (def self.published; where(published: true); end) are functionally identical to scopes. Prefer scopes when conditional logic is involved.
Reference: Active Record Query Interface — Rails Guides
Use Enums for Finite State Fields
Storing status as raw strings allows typos and inconsistent values. Enums provide predefined states with query scopes and predicate methods.
Incorrect (raw string status field):
class Order < ApplicationRecord
end
# Prone to typos and inconsistency
order.update(status: "shiped") # Typo saved to DB
Order.where(status: "pending")
order.status == "pending" # String comparison everywhereCorrect (enum with predefined states):
class Order < ApplicationRecord
enum :status, {
pending: 0,
confirmed: 1,
shipped: 2,
delivered: 3,
cancelled: 4
}
end
order.shipped! # Transition method
order.shipped? # Predicate method
Order.shipped # Auto-generated scope
Order.where.not(status: :cancelled)Benefits:
- Stored as integers (faster queries, less storage)
- Auto-generated scopes, predicates, and bang methods
- Invalid values raise ArgumentError
Reference: Active Record Enums — Rails API
Extract Complex Logic into Service Objects
When business logic spans multiple models or external services, it doesn't belong in any single model. Service objects encapsulate multi-step operations in testable POROs.
Incorrect (multi-model logic in fat model):
class User < ApplicationRecord
def register_with_team(team_name)
transaction do
save!
team = Team.create!(name: team_name, owner: self)
Membership.create!(user: self, team: team, role: "admin")
TeamMailer.welcome(team).deliver_later
AuditLog.record("user_registered", user: self, team: team)
end
end
endCorrect (service object):
# app/services/users/register.rb
class Users::Register
def initialize(user:, team_name:)
@user = user
@team_name = team_name
end
def call
ActiveRecord::Base.transaction do
@user.save!
team = Team.create!(name: @team_name, owner: @user)
Membership.create!(user: @user, team: team, role: "admin")
TeamMailer.welcome(team).deliver_later
AuditLog.record("user_registered", user: @user, team: team)
end
end
endBenefits:
- Testable without creating a User first
- Reusable from controllers, jobs, and rake tasks
- Single responsibility per service
Reference: Rails Service Objects — Toptal
Validate Data at the Model Level
Controller-level validation scatters checks across actions and misses data written by jobs or console. Model validations enforce rules at every entry point.
Incorrect (validation in controller):
class UsersController < ApplicationController
def create
if params[:user][:email].blank? || !params[:user][:email].include?("@")
flash[:alert] = "Invalid email"
return render :new, status: :unprocessable_entity
end
@user = User.create!(user_params) # No model validation — jobs bypass this
redirect_to @user
end
endCorrect (validation in model):
# app/models/user.rb
class User < ApplicationRecord
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { maximum: 100 }
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render :new, status: :unprocessable_entity
end
end
endReference: Active Record Validations — Rails Guides
Authenticate Before Authorize on Every Request
Missing authentication on a single action exposes data. Use before_action in ApplicationController and skip explicitly for public endpoints.
Incorrect (authentication per controller):
class OrdersController < ApplicationController
def index
@orders = current_user.orders # current_user might be nil
end
def show
@order = Order.find(params[:id]) # No auth check, anyone can view
end
endCorrect (default authentication with explicit skips):
class ApplicationController < ActionController::Base
before_action :authenticate_user!
end
class OrdersController < ApplicationController
before_action :authorize_order_access, only: [:show, :edit, :update]
def show
@order = current_user.orders.find(params[:id])
end
private
def authorize_order_access
@order = current_user.orders.find_by(id: params[:id])
head :not_found unless @order
end
end
class PublicPagesController < ApplicationController
skip_before_action :authenticate_user!
endReference: Securing Rails Applications — Rails Guides
Enable CSRF Protection for All Form Submissions
Without CSRF protection, attackers can trick authenticated users into performing actions. Rails includes CSRF tokens by default — never disable it.
Incorrect (CSRF protection disabled):
class ApplicationController < ActionController::Base
skip_forgery_protection # Disables CSRF for ALL controllers
endCorrect (CSRF enabled, API excluded):
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
class Api::BaseController < ActionController::API
# API controllers use token auth instead of CSRF
endNote: For Turbo/Hotwire, CSRF tokens are handled automatically. Ensure <%= csrf_meta_tags %> is in your layout.
Reference: Securing Rails Applications — Rails Guides
Never Interpolate User Input in SQL
String interpolation in SQL allows attackers to execute arbitrary queries. Always use parameterized queries or ActiveRecord's safe query methods.
Incorrect (SQL injection vulnerability):
class SearchController < ApplicationController
def index
@users = User.where("name LIKE '%#{params[:query]}%'") # SQL injection
end
endCorrect (parameterized query):
class SearchController < ApplicationController
def index
@users = User.where("name LIKE ?", "%#{params[:query]}%")
end
endAlternative (sanitize for LIKE):
@users = User.where("name LIKE ?", "%#{User.sanitize_sql_like(params[:query])}%")Reference: Securing Rails Applications — Rails Guides
Scope Queries to Current User for Authorization
Using find(params[:id]) without scoping allows users to access any record by guessing IDs. Scope queries through the current user's associations.
Incorrect (unscoped find exposes all records):
class OrdersController < ApplicationController
def show
@order = Order.find(params[:id]) # Any user can view any order by ID
end
def update
@order = Order.find(params[:id]) # Any user can update any order
@order.update(order_params)
end
endCorrect (scoped through current user):
class OrdersController < ApplicationController
def show
@order = current_user.orders.find(params[:id])
end
def update
@order = current_user.orders.find(params[:id])
@order.update(order_params)
end
endReference: OWASP Rails Cheat Sheet
Whitelist Permitted Params, Never Blacklist
Blacklisting attributes misses new columns added later. Whitelisting ensures only intended attributes are assignable.
Incorrect (blacklist approach):
def user_params
params.require(:user).permit!.except(:admin, :role) # New sensitive columns are exposed
endCorrect (whitelist approach):
def user_params
params.require(:user).permit(:name, :email, :avatar, :bio)
endAlternative (context-dependent params):
def user_params
if current_user.admin?
params.require(:user).permit(:name, :email, :avatar, :bio, :role)
else
params.require(:user).permit(:name, :email, :avatar, :bio)
end
endReference: Action Controller Overview — Rails Guides
Move Display Logic to Helpers or Presenters
Complex conditionals in ERB templates create unreadable views and resist testing. Extract display logic into helpers or presenter objects.
Incorrect (complex logic in template):
<div class="user-badge">
<% if @user.admin? %>
<span class="badge badge-admin">Admin</span>
<% elsif @user.moderator? %>
<span class="badge badge-mod">Moderator</span>
<% elsif @user.premium? %>
<span class="badge badge-premium">Premium</span>
<% else %>
<span class="badge badge-member">Member</span>
<% end %>
<span class="name"><%= @user.first_name %> <%= @user.last_name %></span>
<span class="joined"><%= time_ago_in_words(@user.created_at) %> ago</span>
</div>Correct (helper method):
# app/helpers/users_helper.rb
module UsersHelper
def user_badge(user)
role = if user.admin? then "admin"
elsif user.moderator? then "mod"
elsif user.premium? then "premium"
else "member"
end
tag.span(role.titleize, class: "badge badge-#{role}")
end
end<div class="user-badge">
<%= user_badge(@user) %>
<span class="name"><%= @user.first_name %> <%= @user.last_name %></span>
<span class="joined"><%= time_ago_in_words(@user.created_at) %> ago</span>
</div>Reference: Action View Helpers — Rails Guides
Use Collection Rendering Instead of Loop Partials
Rendering partials inside a loop instantiates ActionView once per iteration. Collection rendering instantiates it once for all items.
Incorrect (partial instantiated per iteration):
<% @orders.each do |order| %>
<%= render partial: "order", locals: { order: order } %>
<% end %>Correct (single instantiation for collection):
<%= render partial: "order", collection: @orders, as: :order %>Alternative (shorthand):
<%= render @orders %>Reference: Layouts and Rendering — Rails Guides
Use form_with Instead of form_tag or form_for
form_tag and form_for still work but are superseded by form_with since Rails 5.1. form_with unifies both APIs, includes CSRF protection, and integrates with Turbo by default.
Incorrect (legacy form helpers):
<%= form_for @order do |f| %>
<%= f.text_field :shipping_address %>
<%= f.submit %>
<% end %>
<%= form_tag search_path, method: :get do %>
<%= text_field_tag :query %>
<%= submit_tag "Search" %>
<% end %>Correct (unified form_with):
<%= form_with model: @order do |f| %>
<%= f.text_field :shipping_address %>
<%= f.submit %>
<% end %>
<%= form_with url: search_path, method: :get do |f| %>
<%= f.text_field :query %>
<%= f.submit "Search" %>
<% end %>Reference: Form Helpers — Rails Guides
Use Turbo Frames for Partial Page Updates
Full page reloads for inline edits and toggles waste bandwidth and break user focus. Turbo Frames replace only the targeted section of the page.
Incorrect (full page reload for edit toggle):
<!-- app/views/tasks/show.html.erb -->
<div class="task">
<h2><%= @task.title %></h2>
<p><%= @task.description %></p>
<%= link_to "Edit", edit_task_path(@task) %>
</div>Correct (inline replacement with Turbo Frame):
<!-- app/views/tasks/show.html.erb -->
<%= turbo_frame_tag @task do %>
<div class="task">
<h2><%= @task.title %></h2>
<p><%= @task.description %></p>
<%= link_to "Edit", edit_task_path(@task) %>
</div>
<% end %>
<!-- app/views/tasks/edit.html.erb -->
<%= turbo_frame_tag @task do %>
<%= render "form", task: @task %>
<% end %>Benefits:
- No JavaScript required
- Automatic frame matching by DOM ID
- Progressive enhancement — works without JavaScript
Reference: Turbo Frames — Hotwire
Use Turbo Streams for Real-Time Page Mutations
Custom JavaScript for DOM manipulation creates fragile, hard-to-maintain code. Turbo Streams declaratively append, prepend, replace, or remove elements.
Incorrect (custom JavaScript for dynamic updates):
<!-- Custom JS to append new comment -->
<script>
fetch("/comments", { method: "POST", body: formData })
.then(response => response.json())
.then(comment => {
const html = `<div class="comment">${comment.body}</div>`;
document.getElementById("comments").insertAdjacentHTML("beforeend", html);
});
</script>Correct (Turbo Stream response):
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
@comment = @post.comments.build(comment_params)
@comment.save
respond_to do |format|
format.turbo_stream
format.html { redirect_to @post }
end
end
end<!-- app/views/comments/create.turbo_stream.erb -->
<%= turbo_stream.append "comments" do %>
<%= render partial: "comments/comment", locals: { comment: @comment } %>
<% end %>Reference: Turbo Streams — Hotwire
Related skills
FAQ
What does rails-dev do?
rails-dev: A skill for development. This provides functionality for development workflows.
When should I use rails-dev?
When you need to use rails-dev for development tasks, or when rails-dev: a skill for development. this provides functionality for development workflows.
What are the main capabilities?
rails-dev.