| name | ruby-development |
| description | Ruby best practices, patterns, and idioms for building elegant, maintainable applications. Use when the task involves `Ruby project`, `Gemfile`, `Ruby on Rails`, `RSpec`, or `Ruby gems`. |
| license | MIT |
| metadata | {"version":"1.0.0"} |
Ruby Development
Production patterns and idioms for Ruby programming, covering project structure, gems, blocks,
testing, metaprogramming, and error handling.
When to Use This Skill
- Starting or structuring a Ruby project
- Working with Bundler and gem dependencies
- Writing idiomatic Ruby with blocks, procs, and lambdas
- Testing with RSpec or Minitest
- Applying Ruby style conventions
- Writing Rake tasks for automation
- Understanding metaprogramming basics
Core Concepts
1. Project Layout
myapp/
├── lib/
│ ├── myapp.rb # Main entry, requires sub-modules
│ └── myapp/
│ ├── client.rb
│ ├── parser.rb
│ └── errors.rb
├── spec/ # RSpec tests
│ ├── spec_helper.rb
│ ├── myapp/
│ │ ├── client_spec.rb
│ │ └── parser_spec.rb
│ └── support/
│ └── shared_contexts.rb
├── bin/
│ └── myapp # CLI executable
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── myapp.gemspec # If building a gem
└── README.md
2. Key Principles
| Principle | Ruby Idiom |
|---|
| Duck typing | Respond to methods, not check class |
| Convention | snake_case methods, PascalCase classes |
| Blocks everywhere | Yield to blocks for callbacks and iteration |
| Open classes | Extend existing classes carefully |
| POLA | Principle of Least Astonishment |
Quick Start
bundle gem myapp
bundle install
bundle exec rspec
bundle exec rake test
bundle exec rubocop
bundle exec irb -r ./lib/myapp
Patterns
Pattern 1: Error Handling
module MyApp
class Error < StandardError; end
class NotFoundError < Error; end
class ValidationError < Error
attr_reader :field
def initialize(field, message)
@field = field
super("Validation failed on #{field}: #{message}")
end
end
class AuthenticationError < Error; end
end
class UserService
def find!(id)
user = repository.find(id)
raise MyApp::NotFoundError, "User #{id} not found" unless user
user
end
def create(params)
validate!(params)
repository.save(User.new(params))
rescue MyApp => e
logger.warn()
=> e
logger.error()
,
()
.new(, ) params[].?
.new(, ) params[].match?()
file = .new()
file
file&.close
file&.unlink
Pattern 2: Blocks, Procs, and Lambdas
def with_logging(label)
puts "[START] #{label}"
result = yield
puts "[END] #{label}"
result
end
with_logging("fetch") { http_client.get(url) }
validator = Proc.new { |val| val.is_a?(String) && val.length > 0 }
validator.call("")
validator.call("hello")
transform = ->(x) { x.upcase.strip }
names.map(&transform)
processor = method(:process_item)
items.each(&processor)
def fetch_all(urls, &block)
urls.map { |url| fetch(url) }.each(&block)
end
fetch_all(urls) { |response| puts response.status }
Pattern 3: Idiomatic Ruby
def process(order)
return unless order.valid?
return if order.cancelled?
order.fulfill
end
active_users = users.select(&:active?)
emails = active_users.map(&:email)
total = orders.sum(&:total)
grouped = items.group_by(&:category)
Coordinate = Struct.new(:lat, :lng, keyword_init: true) do
def to_s
"#{lat}, #{lng}"
end
end
point = Coordinate.new(lat: 40.7, lng: -74.0)
def create_user(name:, email:, role: :member)
User.new(name: name, email: email, role: role)
end
case response
{ , { => items } }
process_items(items)
{ }
handle_not_found
{ (..) }
handle_server_error
Pattern 4: Testing with RSpec
require "spec_helper"
RSpec.describe MyApp::UserService do
subject(:service) { described_class.new(repository: repository) }
let(:repository) { instance_double(MyApp::UserRepository) }
describe "#find!" do
context "when user exists" do
let(:user) { MyApp::User.new(id: "123", name: "Alice") }
before do
allow(repository).to receive(:find).with("123").and_return(user)
end
it "returns the user" do
expect(service.find!("123")).to eq(user)
end
end
context "when user does not exist" do
before do
allow(repository).to receive(:find).with("999").and_return(nil)
end
it "raises NotFoundError" do
expect { service.find!("999") }
.to raise_error(MyApp::NotFoundError, /User 999 not found/)
end
end
describe
let() { { , } }
it
allow(repository).to receive().and_return()
service.create(valid_params)
expect(repository).to have_received().with(
an_instance_of()
)
it
expect { service.create( , ) }
.to raise_error()
Pattern 5: Metaprogramming (Use Sparingly)
module MyApp
module Attributes
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def attribute(name, type: String, default: nil)
define_method(name) do
instance_variable_get(:"@#{name}") || default
end
define_method(:"#{name}=") do |value|
unless value.is_a?(type)
raise TypeError, "Expected #{type}, got #{value.class}"
end
instance_variable_set(:"@#{name}", value)
end
end
end
end
class Config
include Attributes
attribute :host, type: ,
attribute , ,
attribute , ,
()
= data
()
key = name.to_s.chomp().to_sym
name.to_s.end_with?()
[key] = args.first
.key?(key)
[key]
()
.key?(name.to_s.chomp().to_sym) ||
Pattern 6: Rake Tasks
require "bundler/gem_tasks"
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new(:spec)
task default: :spec
namespace :db do
desc "Run database migrations"
task :migrate do
require_relative "lib/myapp"
MyApp::Database.migrate!
puts "Migrations complete"
end
desc "Seed the database"
task seed: :migrate do
MyApp::Database.seed!
puts "Seed complete"
end
desc "Reset database"
task reset: [:drop, :migrate, :seed]
desc "Drop database"
task :drop do
MyApp::Database.drop!
puts "Database dropped"
end
end
Best Practices
Do's
- Use
frozen_string_literal: true — Prevents accidental string mutation, improves performance
- Prefer keyword arguments — For methods with more than 2 parameters
- Write guard clauses — Early returns keep code flat and readable
- Use
Enumerable methods — map, select, reduce over manual loops
- Pair
method_missing with respond_to_missing? — Always
- Freeze constants —
DEFAULTS = { timeout: 30 }.freeze
- Use
bundle exec — To ensure correct gem versions
Don'ts
- Don't rescue
Exception — Catches SignalException, SystemExit; rescue StandardError
instead
- Don't monkey-patch in production — Open classes are powerful but dangerous
- Don't use
eval with user input — Security risk
- Don't ignore
Rubocop warnings — Fix or explicitly disable with comments
- Don't overuse metaprogramming — Clever code is hard to debug and maintain
- Don't mutate method arguments — Use
.dup or .freeze defensively
Resources