home / skills / renzo4web / automaton / programming-rails

programming-rails skill

/.agents/skills/programming-rails

This skill helps you apply Rails best practices across models, controllers, services, and jobs to improve structure, performance, and security.

npx playbooks add skill renzo4web/automaton --skill programming-rails

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
11.2 KB
---
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.

```ruby
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 `:dependent` option for `has_many` and `has_one`
- Use `:inverse_of` for 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

```ruby
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

```ruby
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.

```ruby
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`.

```ruby
# 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.

```ruby
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.

```ruby
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).

```ruby
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.

```ruby
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.

```ruby
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.

```ruby
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.

```ruby
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.

```ruby
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.

```ruby
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.

```ruby
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.

```ruby
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.

```ruby
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:

1. **Models**: Validations complete? Associations configured with `:dependent`? Queries optimized with `includes`?
2. **Controllers**: RESTful actions only? Strong params filtering? Error handling with `rescue_from`?
3. **Services**: Single responsibility? Uses transactions? Handles errors gracefully?
4. **Jobs**: Idempotent? Retries configured for transient errors? Efficient batch processing?
5. **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.

Overview

This skill codifies Rails best practices for models, controllers, service objects, and background jobs to build maintainable, secure, and performant applications. It emphasizes convention over configuration, thin controllers, well-structured models, and resilient background processing. The guidance helps teams decide where to place logic and how to structure code for testability and scalability.

How this skill works

The skill inspects common Rails concerns and prescribes patterns: consistent model layout (constants, associations, validations, scopes, callbacks, methods), RESTful controllers with strong parameters, service and query objects for business logic, and idempotent ActiveJob workers. It also recommends database-level constraints, eager loading to avoid N+1 queries, and retry/error strategies for background jobs. Examples and patterns illustrate typical implementations.

When to use it

  • When organizing or refactoring models to keep business rules clear and testable
  • When building controllers for web or API endpoints and enforcing strong parameters
  • When extracting complex business behavior to service or query objects
  • When designing background jobs that must be idempotent and retry-safe
  • When integrating external APIs or adding robust error handling and retries

Best practices

  • Keep controllers thin: push complex logic into services or models
  • Structure models predictably and always specify dependent behavior for associations
  • Use scopes and query objects to encapsulate database logic and avoid N+1 with includes
  • Wrap multi-step operations in transactions and return clear Result objects from services
  • Make jobs idempotent, configure targeted retries, and avoid side effects in callbacks
  • Add indexes and database constraints for critical validations and frequent queries

Example use cases

  • Extracting order creation (payments, items, notifications) into a CreateOrder service wrapped in a transaction
  • Implementing paginated index endpoints with includes to prevent N+1 and strong params for filtering
  • Creating a ProductSearch query object to consolidate complex filtering and sorting
  • Writing ProcessOrderJob that safely retries and skips already processed orders
  • Wrapping external HTTP calls in a service with explicit error handling and test doubles

FAQ

Where should I put business validation: model or service?

Use model validations for data integrity and simple domain rules; extract cross-entity or multi-step business processes into service objects.

How do I prevent N+1 queries?

Use includes/preload/eager_load for associations in queries and consider query objects to centralize eager-loading logic.