Ruby on Rails development for both legacy (4.x-6.x) and modern (7.x-8.x) apps, including Docker, RSpec, migrations, and security.
Existing (legacy) projects:
Gemfile, Gemfile.lock, or .ruby-version.New projects:
rails new <app_name> --database=<db_engine> \
--skip-action-mailbox --skip-action-text --skip-active-storage \
--skip-action-cable --skip-test --skip-system-test
rails generate rspec:install.When docker-compose.yml or compose.yml exists, Docker is the default runtime for linting, tests, and Rails commands.
# Run any command
docker compose run --rm -e RUBYOPT='-W0' <container_name> <command>
# Common commands
docker compose run --rm -e RUBYOPT='-W0' <container_name> rails c
docker compose run --rm -e RUBYOPT='-W0' <container_name> rails db:migrate
docker compose run --rm -e RUBYOPT='-W0' <container_name> rspec <spec_path>
docker compose run --rm -e RUBYOPT='-W0' <container_name> rails g <generator> <args>
docker compose run --rm -e RUBYOPT='-W0' <container_name> bundle exec rubocop <file> --autocorrect
# frozen_string_literal: true at top of all Ruby files (except migrations, patches, specs).->).case statements must have an else clause.app/services/; concerns in app/models/concerns/ and app/controllers/concerns/.ApplicationRecord base class for models.belongs_to required by default (use optional: true if needed).ApplicationRecord — use ActiveRecord::Base directly.attr_accessible for mass assignment protection.before_filter instead of before_action.Choose sync vs. async based on test scope when running inside Docker:
| Scope | Mode | Example |
|---|---|---|
Full app or broad suite (spec/, spec/models/, spec/controllers/, etc.) | Background / async | docker compose run --rm -e RUBYOPT='-W0' <service> bundle exec rspec spec/ |
| Single file or focused component | Synchronous | docker compose run --rm -e RUBYOPT='-W0' <service> bundle exec rspec spec/models/user_spec.rb |
Decision rule: No path or multi-directory path → background/async. Single file or single tightly-scoped directory → synchronous.
rails_helper.let instead of instance variables; use is_expected over should.render_views.ActiveSupport::TimeHelpers for time stubbing (in around blocks).# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ModelName, type: :model do
subject { build(:model_name) }
describe 'validations' do
it { is_expected.to validate_presence_of(:attribute) }
end
describe '#method_name' do
let(:instance) { create(:model_name) }
it 'does something' do
expect(instance.method_name).to eq(expected_value)
end
end
end
describe '#action' do
it 'returns expected JSON' do
get :action, params: { id: record.id }, format: :json
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['key']).to eq(expected_value)
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :model_name do
attribute { Faker::Lorem.word }
association :related_model
end
end
Priority: change > up/down > never self.up/self.down.
# Preferred
def change
add_column :table_name, :column_name, :string, null: false, default: ''
end
# Non-reversible
def up
change_column :table_name, :column_name, :text
end
def down
change_column :table_name, :column_name, :string
end
deleted_at) — never hard-delete user-facing records.class PatchName < Patch
def run
start
perform
stop
end
private
def perform
# Data manipulation logic
end
end
protect_from_forgery.html_safe unless absolutely necessary.config.force_ssl = true in production.attr_encrypted for field-level encryption (e.g., bank account numbers).SecureRandom for tokens.includes, preload, eager_load).app/assets/javascripts.$(document).on('ready', function() { ... }) for DOM ready.const for constants, let for locals; avoid var.docker compose logs -f <container_name>byebug or binding.pry for breakpoints.ActiveRecord::Base.connection.execute if needed.For complex UI flows, create test data scripts in .local/, tmp/scripts/, or another ignored path:
# frozen_string_literal: true
# DO NOT MERGE THIS FILE
# Purpose: Manual browser testing for [Feature Name]
# Run: docker compose run --rm -e RUBYOPT='-W0' app rails runner .local/script_name.rb
# CLEANUP
puts '=== Cleaning up existing test data ==='
ChildModel.where(parent_id: parent_ids).destroy_all
ParentModel.where(code: test_codes).destroy_all
# CREATE
puts '=== Creating test data ==='
items.each_with_index do |item, index|
unique_id = 90000 + index
Model.create!(external_id: unique_id, **item)
end
# VERIFY
puts '=== Verification ==='
results = ServiceClass.new(params).fetch
puts results.any? ? '✅ Found!' : '❌ Not found!'
puts "URL: /controller/action/#{record.id}"
Naming: <feature>_test_data.rb (e.g., orders_test_data.rb). Always add # DO NOT MERGE THIS FILE header.