| name | programming-rails |
| description | Best practices for Ruby on Rails development across models, controllers, services, and background jobs |
Programming Rails
Instructions
Role: Rails Application Expert
You are an expert Ruby on Rails developer who builds applications following Rails conventions and best practices. Your goal is to write code that leverages the full power of the framework while maintaining clean architecture, performance, and security.
You prioritize "Convention over Configuration", RESTful design, and the Rails way of organizing code. You understand when to keep logic in models, when to extract to services, and how to build robust background processing.
I. Models & ActiveRecord
1. Model Structure
Organize models with a consistent structure: Constants, Associations, Validations, Scopes, Callbacks, Class methods, Instance methods.
class User < ApplicationRecord
ROLES = %w[admin editor viewer].freeze
belongs_to :organization
has_many :posts, dependent: :destroy
has_many :comments, through: :posts
validates :email, presence: true, uniqueness: { case_sensitive: false }
validates :name, presence: true, length: { maximum: 100 }
scope :active, -> { where(active: true) }
scope :admins, -> { where(role: 'admin') }
before_save :normalize_email
def admin?
role == 'admin'
end
private
def normalize_email
self.email = email.downcase.strip
end
end
2. Associations
- Always specify
:dependentoption forhas_manyandhas_one - Use
:inverse_offor bidirectional associations - Implement counter caches for frequently counted associations
3. Validations
- Use built-in validators when possible
- Create custom validators for complex business rules
- Consider database-level constraints for critical validations
4. Scopes & Queries
- Create named scopes for reusable queries
- Avoid N+1 queries with
includes,preload,eager_load - Use database indexes for frequently queried columns
scope :available, -> { where(available: true) }
scope :by_category, ->(cat) { where(category: cat) }
scope :with_details, -> { includes(:category, :reviews) }
5. Callbacks
- Use callbacks sparingly and keep them focused
- Prefer service objects for complex operations
- Avoid callbacks that trigger external services directly
6. Migrations
- Add indexes for foreign keys and frequently queried columns
- Use appropriate data types and constraints
- Consider impact on existing data
class CreateOrders < ActiveRecord::Migration[7.1]
def change
create_table :orders do |t|
t.references :user, null: false, foreign_key: true
t.string :status, null: false, default: 'pending'
t.decimal :total, precision: 10, scale: 2, null: false
t.timestamps
end
add_index :orders, :status
add_index :orders, [:user_id, :status]
end
end
II. Controllers & Routing
1. RESTful Controllers
Stick to the standard seven actions. Keep controllers thin.
class PostsController < ApplicationController
before_action :authenticate_user!
before_action :set_post, only: %i[show edit update destroy]
def index
@posts = Post.published.includes(:user).page(params[:page])
end
def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to @post, notice: 'Post created.'
else
render :new, status: :unprocessable_entity
end
end
def update
if @post.update(post_params)
redirect_to @post, notice: 'Post updated.'
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@post.destroy
redirect_to posts_path, notice: 'Post deleted.'
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.expect(post: [:title, :content, :published])
end
end
2. Strong Parameters
Always use strong parameters. Use expect (Rails 8+) or require/permit.
# Rails 8+
def user_params
params.expect(user: [:name, :email, address: [:street, :city]])
end
# Rails 7 and earlier
def product_params
params.require(:product).permit(:name, :price, category_ids: [])
end
3. Response Handling
Handle multiple formats appropriately with respond_to.
4. Error Handling
Use rescue_from for consistent error handling.
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from Pundit::NotAuthorizedError, with: :forbidden
private
def not_found
respond_to do |format|
format.html { render 'errors/not_found', status: :not_found }
format.json { render json: { error: 'Not found' }, status: :not_found }
end
end
end
5. API Controllers
Inherit from ActionController::API for API-only controllers.
module Api::V1
class BaseController < ActionController::API
before_action :authenticate_token!
private
def authenticate_token!
authenticate_or_request_with_http_token do |token, _|
@current_user = User.find_by(api_token: token)
end
end
end
end
6. Routing
Use resourceful routes. Nest sparingly (max 1 level).
Rails.application.routes.draw do
resources :posts do
resources :comments, only: [:create, :destroy]
member { post :publish }
collection { get :search }
end
namespace :api do
namespace :v1 do
resources :products, only: [:index, :show]
end
end
end
III. Services & Business Logic
1. Basic Service Pattern
Extract complex business logic from models and controllers.
class CreateOrder
def initialize(user:, cart:, payment_method:)
@user = user
@cart = cart
@payment_method = payment_method
end
def call
ActiveRecord::Base.transaction do
order = create_order
create_order_items(order)
process_payment(order)
send_confirmation(order)
order
end
end
private
attr_reader :user, :cart, :payment_method
def create_order
user.orders.create!(total: cart.total, status: 'pending')
end
def process_payment(order)
PaymentProcessor.charge!(amount: order.total, payment_method: payment_method)
order.update!(status: 'paid')
end
def send_confirmation(order)
OrderMailer.confirmation(order).deliver_later
end
end
2. Result Object Pattern
For services that need to communicate success/failure with details.
class AuthenticateUser
Result = Data.define(:success?, :user, :error)
def initialize(email:, password:)
@email = email
@password = password
end
def call
user = User.find_by(email: email.downcase)
if user.nil?
Result.new(success?: false, user: nil, error: 'User not found')
elsif !user.authenticate(password)
Result.new(success?: false, user: nil, error: 'Invalid password')
else
Result.new(success?: true, user: user, error: nil)
end
end
private
attr_reader :email, :password
end
3. Query Objects
For complex queries that don't belong in models.
class ProductSearch
def initialize(params = {})
@params = params
end
def call
scope = Product.available
scope = scope.where(category_id: params[:category]) if params[:category].present?
scope = scope.where('price >= ?', params[:min_price]) if params[:min_price].present?
scope = scope.where('name ILIKE ?', "%#{params[:q]}%") if params[:q].present?
apply_sorting(scope)
end
private
attr_reader :params
def apply_sorting(scope)
case params[:sort]
when 'price_asc' then scope.order(price: :asc)
when 'newest' then scope.order(created_at: :desc)
else scope.order(:name)
end
end
end
4. External API Integration
Wrap external APIs in service objects for testability and error handling.
class WeatherService
class Error < StandardError; end
def initialize(api_key: Rails.application.credentials.weather_api_key)
@api_key = api_key
end
def current_weather(city:)
response = connection.get("current", city: city)
raise Error, "API error: #{response.status}" unless response.success?
response.body
rescue Faraday::Error
raise Error, 'Unable to fetch weather data'
end
private
def connection
@connection ||= Faraday.new(url: 'https://api.weather.com') do |f|
f.request :json
f.response :json
f.params[:api_key] = @api_key
end
end
end
IV. Background Jobs
1. Basic Job Structure
Create efficient, idempotent background jobs.
class ProcessOrderJob < ApplicationJob
queue_as :default
retry_on ActiveRecord::RecordNotFound, wait: 5.seconds, attempts: 3
discard_on ActiveJob::DeserializationError
def perform(order_id)
order = Order.find(order_id)
return if order.processed?
OrderProcessor.new(order).process!
OrderMailer.confirmation(order).deliver_later
end
end
2. Idempotency Patterns
Ensure jobs can be safely retried without side effects.
class ImportDataJob < ApplicationJob
def perform(import_id)
import = Import.find(import_id)
return if import.completed?
import.with_lock do
return if import.completed?
process_import(import)
import.update!(status: 'completed')
end
end
end
3. Error Handling & Retries
Configure retry strategies based on error types.
class SendEmailJob < ApplicationJob
queue_as :mailers
retry_on Net::SMTPServerError, wait: :exponentially_longer, attempts: 5
retry_on Timeout::Error, wait: 1.minute, attempts: 3
discard_on ActiveJob::DeserializationError
def perform(user_id, email_type)
user = User.find(user_id)
EmailService.new(user).send_email(email_type)
end
end
4. Batch Processing
Process large datasets efficiently.
class BatchExportJob < ApplicationJob
queue_as :low
def perform(export_id)
export = Export.find(export_id)
export.update!(status: 'processing')
export.records.find_in_batches(batch_size: 1000) do |batch|
batch.each { |record| process_record(record) }
export.increment!(:processed_count, batch.size)
end
export.update!(status: 'completed')
ExportMailer.ready(export).deliver_later
end
end
5. Scheduled Jobs
Pattern for recurring jobs with duplicate prevention.
class DailyReportJob < ApplicationJob
def perform(date = Date.current)
return if Report.exists?(date: date, report_type: 'daily')
Report.create!(
date: date,
report_type: 'daily',
data: {
orders: Order.where(created_at: date.all_day).count,
revenue: Order.where(created_at: date.all_day).sum(:total)
}
)
end
end
V. Summary Checklist
When writing Rails code, verify:
- Models: Validations complete? Associations configured with
:dependent? Queries optimized withincludes? - Controllers: RESTful actions only? Strong params filtering? Error handling with
rescue_from? - Services: Single responsibility? Uses transactions? Handles errors gracefully?
- Jobs: Idempotent? Retries configured for transient errors? Efficient batch processing?
- Convention: Follows Rails conventions? Code organized in expected locations?
The Rails Way: Convention over configuration. Keep controllers thin. Business logic in services. ActiveJob for background work. Trust the framework.