| name | rails-development |
| description | Ruby on Rails best practices and conventions covering MVC patterns, ActiveRecord, RESTful routing, testing with RSpec, background jobs, and production-ready Rails development. Use when the task involves `Ruby on Rails`, `Rails project`, `ActiveRecord`, `Rails migrations`, or `Rails API`. |
| license | MIT |
| metadata | {"version":"1.0.0"} |
When to Use
- Building or refactoring a Ruby on Rails application.
- Writing ActiveRecord models with validations, associations, and scopes.
- Setting up RESTful routes and controllers following Rails conventions.
- Writing tests with RSpec and FactoryBot.
- Configuring background jobs with Sidekiq or ActiveJob.
- Optimizing database queries to prevent N+1 problems.
Critical Patterns
- Convention Over Configuration: Follow Rails naming conventions strictly. Model
User maps to
table users, controller UsersController in users_controller.rb. Fighting conventions creates
maintenance nightmares.
- Fat Model, Skinny Controller: Controllers handle HTTP flow only — delegate business logic to
models, service objects, or concerns.
- Prevent N+1 Queries: ALWAYS use
includes, preload, or eager_load when accessing
associations in collections. Use bullet gem in development to detect violations.
- Strong Parameters: NEVER trust user input. Whitelist permitted params in every controller
action.
- Database-Level Constraints: Add validations in the model AND enforce them at the database
level with migration constraints (
null: false, unique indexes, foreign keys).
- Background Jobs for Slow Work: Anything over 100ms that isn't the core response (emails, file
processing, API calls) goes into a background job.
Code Examples
Model with Validations and Associations
class User < ApplicationRecord
has_many :posts, dependent: :destroy
has_many :comments, through: :posts
has_one :profile, dependent: :destroy
belongs_to :organization, optional: true
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
validates :role, inclusion: { in: %w[user admin moderator] }
scope :active, -> { where(deactivated_at: nil) }
scope :admins, -> { where(role: "admin") }
scope :created_after, ->(date) { where(, date) }
scope , ->(query) {
where(, )
}
before_save
.email = email.downcase.strip
Migration with Proper Constraints
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false
t.string :role, null: false, default: "user"
t.references :organization, foreign_key: true
t.datetime :deactivated_at
t.timestamps
end
add_index :users, :email, unique: true
add_index :users, :role
add_index :users, :deactivated_at
end
end
Controller with Strong Parameters
module Api
module V1
class UsersController < ApplicationController
before_action :authenticate_user!
before_action :set_user, only: %i[show update destroy]
before_action :authorize_admin!, only: %i[destroy]
def index
@users = User.active
.includes(:organization, :profile)
.order(created_at: :desc)
.page(params[:page])
render json: @users, each_serializer: UserSerializer
end
def show
render json: @user, serializer: UserDetailSerializer
end
def create
@user = User.new(user_params)
if @user.save
UserMailer.welcome_email(@user).deliver_later
render json: @user,
render { .errors.full_messages },
.update(user_params)
render
render { .errors.full_messages },
= .find(params[])
params.().permit(, , , )
RESTful Routing
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users, only: %i[index show create update destroy] do
member do
patch :deactivate
end
collection do
get :search
end
end
resources :posts do
resources :comments, only: %i[index create destroy], shallow: true
end
end
end
get "up" => "rails/health#show", as: :rails_health_check
end
Preventing N+1 Queries
users = User.all
users.each { |u| puts u.posts.count }
users = User.includes(:posts).all
users.each { |u| puts u.posts.size }
users = User.left_joins(:posts)
.select("users.*, COUNT(posts.id) AS posts_count")
.group("users.id")
class Post < ApplicationRecord
belongs_to :user, counter_cache: true
end
Service Object Pattern
module Users
class CreateService
def initialize(params:, current_user:)
@params = params
@current_user = current_user
end
def call
user = User.new(@params)
ActiveRecord::Base.transaction do
user.save!
user.create_profile!
AuditLog.record!(action: "user.created", actor: @current_user, target: user)
end
UserMailer.welcome_email(user).deliver_later
ServiceResult.new(success: true, data: user)
rescue ActiveRecord::RecordInvalid => e
ServiceResult.new(success: false, errors: e.record.errors.full_messages)
end
end
end
RSpec Tests
RSpec.describe User, type: :model do
describe "validations" do
subject { build(:user) }
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
it { is_expected.to validate_presence_of(:name) }
end
describe "associations" do
it { is_expected.to have_many(:posts).dependent(:destroy) }
it { is_expected.to belong_to(:organization).optional }
end
describe ".active" do
it "excludes deactivated users" do
active_user = create(:user, deactivated_at: nil)
create(:user, deactivated_at: 1.day.ago)
expect(User.active).to eq([active_user])
end
end
end
RSpec.describe "Api::V1::Users", type: :request do
let(:admin) { create(:user, role: "admin") }
let(:headers) { auth_headers(admin) }
describe "GET /api/v1/users" do
it "returns paginated active users"
create_list(, )
get , headers
expect(response).to have_http_status()
expect(json_response.size).to eq()
describe
let() { { attributes_for() } }
it
expect {
post , valid_params, headers
}.to change(, ).by()
. have_enqueued_mail(, )
expect(response).to have_http_status()
Background Jobs
class ExportUsersJob < ApplicationJob
queue_as :default
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
discard_on ActiveJob::DeserializationError
def perform(user_id, format: "csv")
user = User.find(user_id)
export = UserExportService.new(user, format:).call
UserMailer.export_ready(user, export.url).deliver_later
end
end
ExportUsersJob.perform_later(current_user.id, format: "csv")
Best Practices
DO
- Run
bundle exec rubocop and follow the Ruby Style Guide.
- Use
find_each instead of each when iterating over large record sets (batches of 1000).
- Add database indexes for columns used in
WHERE, ORDER BY, and JOIN clauses.
- Use
freeze on string constants to avoid allocations: ROLE_ADMIN = "admin".freeze.
- Scope secrets with
Rails.application.credentials (encrypted) — never commit .env files.
- Write request specs over controller specs — they test the full middleware stack.
- Use
ActiveRecord::Base.transaction for operations that must succeed or fail together.
DON'T
- DON'T use
update_all or delete_all without understanding they skip callbacks and validations.
- DON'T put query logic in views or controllers — use scopes or query objects.
- DON'T use
default_scope — it's global and nearly impossible to override cleanly.
- DON'T call
.count on preloaded associations — use .size (which uses the preloaded data) or
.length.
- DON'T use
after_save callbacks for side effects like sending emails — use service objects or
jobs.
- DON'T write migrations that are not reversible — always provide
up and down or use reversible
methods.
- DON'T skip
null: false constraints in migrations when the model validates presence — the DB is
the last line of defense.
Rails Console Tips
reload!
ActiveRecord::Base.logger = Logger.new(STDOUT)
User.includes(:posts).where(active: true).explain