Claude
Skills
Sign in
Back

rails-framework

Included with Lifetime
$97 forever

# Rails Framework Skill - Quick Reference

General

What this skill does

# Rails Framework Skill - Quick Reference

**Framework**: Ruby on Rails 7+
**For Agent**: backend-developer
**Purpose**: Fast lookup of common Rails patterns and conventions

---

## 1. Rails MVC Patterns

### Controllers (RESTful Actions)

```ruby
class PostsController < ApplicationController
  before_action :set_post, only: %i[show edit update destroy]
  before_action :authenticate_user!, except: %i[index show]

  def index
    @posts = Post.published.order(created_at: :desc).page(params[:page])
  end

  def show
    # @post set by before_action
  end

  def new
    @post = Post.new
  end

  def create
    @post = current_user.posts.build(post_params)
    if @post.save
      redirect_to @post, notice: 'Post created successfully.'
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    # @post set by before_action
  end

  def update
    if @post.update(post_params)
      redirect_to @post, notice: 'Post updated successfully.'
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @post.destroy
    redirect_to posts_url, notice: 'Post deleted successfully.'
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end

  def post_params
    params.require(:post).permit(:title, :body, :published, :category_id, tag_ids: [])
  end
end
```

### Models (Active Record)

```ruby
class Post < ApplicationRecord
  # Associations
  belongs_to :author, class_name: 'User'
  belongs_to :category
  has_many :comments, dependent: :destroy
  has_many :taggings, dependent: :destroy
  has_many :tags, through: :taggings

  # Validations
  validates :title, presence: true, length: { minimum: 5, maximum: 200 }
  validates :body, presence: true
  validates :author, presence: true

  # Scopes
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc) }
  scope :by_author, ->(user) { where(author: user) }

  # Callbacks
  before_save :generate_slug
  after_create :notify_subscribers

  # Class methods
  def self.search(query)
    where('title ILIKE ? OR body ILIKE ?', "%#{query}%", "%#{query}%")
  end

  # Instance methods
  def published?
    published && published_at.present?
  end

  private

  def generate_slug
    self.slug = title.parameterize if title_changed?
  end

  def notify_subscribers
    NotifySubscribersJob.perform_later(id)
  end
end
```

### Routes

```ruby
Rails.application.routes.draw do
  # RESTful resources
  resources :posts do
    resources :comments, only: %i[create destroy]
    member do
      post :publish
      post :unpublish
    end
    collection do
      get :search
    end
  end

  # Namespaced API routes
  namespace :api do
    namespace :v1 do
      resources :posts, only: %i[index show create update destroy]
    end
  end

  # Custom routes
  get '/about', to: 'pages#about'
  root 'posts#index'
end
```

---

## 2. Service Objects

### Basic Service Pattern

```ruby
# app/services/create_post_service.rb
class CreatePostService
  def initialize(user, params)
    @user = user
    @params = params
  end

  def call
    post = @user.posts.build(@params)

    ActiveRecord::Base.transaction do
      if post.save
        notify_subscribers(post)
        update_user_stats(@user)
        Result.success(post)
      else
        Result.failure(post.errors)
      end
    end
  rescue StandardError => e
    Result.failure(errors: [e.message])
  end

  private

  def notify_subscribers(post)
    NotifySubscribersJob.perform_later(post.id)
  end

  def update_user_stats(user)
    user.increment!(:posts_count)
  end
end

# Usage in controller
def create
  result = CreatePostService.new(current_user, post_params).call

  if result.success?
    redirect_to result.value, notice: 'Post created.'
  else
    @post = Post.new(post_params)
    @post.errors.merge!(result.errors)
    render :new, status: :unprocessable_entity
  end
end
```

### Result Object

```ruby
# app/services/result.rb
class Result
  attr_reader :value, :errors

  def initialize(success:, value: nil, errors: nil)
    @success = success
    @value = value
    @errors = errors || []
  end

  def success?
    @success
  end

  def failure?
    !@success
  end

  def self.success(value = nil)
    new(success: true, value: value)
  end

  def self.failure(errors)
    new(success: false, errors: errors)
  end
end
```

---

## 3. Background Jobs

### Active Job with Sidekiq

```ruby
# app/jobs/send_welcome_email_job.rb
class SendWelcomeEmailJob < ApplicationJob
  queue_as :emails

  retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
  discard_on ActiveJob::DeserializationError

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome_email(user).deliver_now
  rescue ActiveRecord::RecordNotFound => e
    # User deleted before job ran
    Rails.logger.warn("User #{user_id} not found: #{e.message}")
  end
end

# Enqueue jobs
SendWelcomeEmailJob.perform_later(user.id)                    # Async
SendWelcomeEmailJob.perform_now(user.id)                      # Sync
SendWelcomeEmailJob.set(wait: 1.hour).perform_later(user.id)  # Delayed
```

### Sidekiq Worker (Direct)

```ruby
# app/workers/data_import_worker.rb
class DataImportWorker
  include Sidekiq::Worker

  sidekiq_options queue: :imports, retry: 5, backtrace: true

  def perform(file_path)
    import_data(file_path)
  rescue StandardError => e
    Rails.logger.error("Import failed: #{e.message}")
    raise  # Re-raise to trigger retry
  end

  private

  def import_data(file_path)
    # Import logic here
  end
end
```

### Sidekiq Configuration

```yaml
# config/sidekiq.yml
:concurrency: 5
:queues:
  - critical
  - default
  - emails
  - imports
  - low

# Retry settings
:max_retries: 5
:timeout: 30
```

---

## 4. Database & Migrations

### Creating Migrations

```ruby
# Generate migration
rails generate migration CreatePosts title:string body:text published:boolean author:references

# db/migrate/20251022000000_create_posts.rb
class CreatePosts < ActiveRecord::Migration[7.0]
  def change
    create_table :posts do |t|
      t.string :title, null: false
      t.text :body, null: false
      t.boolean :published, default: false, null: false
      t.references :author, null: false, foreign_key: { to_table: :users }
      t.string :slug, index: { unique: true }

      t.timestamps
    end

    add_index :posts, :published
    add_index :posts, [:author_id, :created_at]
  end
end

# Add column migration
class AddCategoryToPosts < ActiveRecord::Migration[7.0]
  def change
    add_reference :posts, :category, foreign_key: true, index: true
  end
end

# Index migration
class AddIndexToPostsTitle < ActiveRecord::Migration[7.0]
  def change
    add_index :posts, :title
    # For full-text search
    execute "CREATE INDEX posts_title_gin_trgm_idx ON posts USING gin(title gin_trgm_ops)"
  end
end
```

### Query Optimization (N+1 Prevention)

```ruby
# Bad: N+1 query (1 query for posts + N queries for authors)
posts = Post.all
posts.each do |post|
  puts post.author.name  # Triggers separate query for each post
end

# Good: Eager loading with includes
posts = Post.includes(:author).all
posts.each do |post|
  puts post.author.name  # No additional queries
end

# Preload multiple associations
Post.includes(:author, :comments, :tags).all

# Joins (when you need to filter by association)
Post.joins(:author).where(users: { active: true })

# Left outer joins (include posts without authors)
Post.left_joins(:comments).group(:id).select('posts.*, COUNT(comments.id) as comments_count')
```

### Advanced Queries

```ruby
# Scopes with arguments
class Post < ApplicationRecord
  scope :published_after, ->(date) { where('published_at > ?', date) }
  scope :by_category, ->(category) { where(category: category) }
  scope :search, ->(query) { where('title ILIKE ?', "%#{query}%") }
end

# Chaining scopes
Post.published.by_category('tech').search('rails')

# Subqueries
popular_posts = Post.select(:id).where('views_count > ?', 1
Files: 17
Size: 188.7 KB
Complexity: 51/100
Category: General

Related in General