Provides quick reference for Ruby on Rails 7+ patterns including RESTful controllers with before_actions and strong params, Active Record models with associations validations scopes, and routes.
npx claudepluginhub fortiumpartners/ai-meshThis skill uses the workspace's default tool permissions.
**Framework**: Ruby on Rails 7+
PATTERNS-EXTRACTED.mdREADME.mdREFERENCE.mdVALIDATION.mdexamples/README.mdexamples/background-jobs.example.rbexamples/blog-api.example.rbtemplates/controller.template.rbtemplates/job.template.rbtemplates/migration.template.rbtemplates/model.template.rbtemplates/serializer.template.rbtemplates/service.template.rbtemplates/spec.template.rbProvides Rails Action Controller patterns for routing, before_action filters, strong parameters, and REST conventions. Useful for building robust MVC controllers.
Develops Ruby on Rails applications with models, controllers, views, Active Record ORM, authentication, and RESTful routes. Use for building Rails apps, managing database relationships, and MVC architecture.
Optimizes Ruby on Rails code for performance and maintainability with 45 rules on controllers, models, ActiveRecord queries, caching, views, APIs, security, and background jobs. Use when writing, reviewing, or refactoring Rails apps.
Share bugs, ideas, or general feedback.
Framework: Ruby on Rails 7+ For Agent: backend-developer Purpose: Fast lookup of common Rails patterns and conventions
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
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
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
# 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
# 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
# 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
# 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
# config/sidekiq.yml
:concurrency: 5
:queues:
- critical
- default
- emails
- imports
- low
# Retry settings
:max_retries: 5
:timeout: 30
# 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
# 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')
# 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 > ?', 1000)
Comment.where(post_id: popular_posts)
# Aggregation
Post.group(:category_id).count
Post.group(:category_id).average(:views_count)
Post.group(:category_id).sum(:likes_count)
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ActionController::API
before_action :authenticate_api_user!
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def authenticate_api_user!
token = request.headers['Authorization']&.split(' ')&.last
@current_api_user = User.find_by(api_token: token)
render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_api_user
end
def not_found
render json: { error: 'Not found' }, status: :not_found
end
def bad_request
render json: { error: 'Bad request' }, status: :bad_request
end
end
end
end
# app/controllers/api/v1/posts_controller.rb
module Api
module V1
class PostsController < BaseController
skip_before_action :authenticate_api_user!, only: %i[index show]
def index
@posts = Post.published.page(params[:page]).per(params[:per_page] || 20)
render json: @posts, each_serializer: PostSerializer
end
def show
@post = Post.find(params[:id])
render json: @post, serializer: PostSerializer
end
def create
@post = @current_api_user.posts.build(post_params)
if @post.save
render json: @post, serializer: PostSerializer, status: :created
else
render json: { errors: @post.errors }, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title, :body, :published, :category_id)
end
end
end
end
# app/serializers/post_serializer.rb
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body, :slug, :published_at, :created_at
belongs_to :author, serializer: AuthorSerializer
has_many :comments, serializer: CommentSerializer
def published_at
object.published_at&.iso8601
end
end
# app/serializers/author_serializer.rb
class AuthorSerializer < ActiveModel::Serializer
attributes :id, :name, :email, :avatar_url
end
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :posts, only: %i[index show create update destroy] do
resources :comments, only: %i[index create destroy]
end
resources :users, only: %i[show update]
post '/auth/login', to: 'authentication#login'
delete '/auth/logout', to: 'authentication#logout'
end
end
end
# spec/models/post_spec.rb
require 'rails_helper'
RSpec.describe Post, type: :model do
describe 'associations' do
it { should belong_to(:author).class_name('User') }
it { should belong_to(:category) }
it { should have_many(:comments).dependent(:destroy) }
it { should have_many(:tags).through(:taggings) }
end
describe 'validations' do
it { should validate_presence_of(:title) }
it { should validate_presence_of(:body) }
it { should validate_length_of(:title).is_at_least(5).is_at_most(200) }
end
describe 'scopes' do
let!(:published_post) { create(:post, published: true) }
let!(:draft_post) { create(:post, published: false) }
it 'returns only published posts' do
expect(Post.published).to include(published_post)
expect(Post.published).not_to include(draft_post)
end
end
describe '#published?' do
it 'returns true when post is published' do
post = build(:post, published: true, published_at: 1.day.ago)
expect(post).to be_published
end
it 'returns false when post is not published' do
post = build(:post, published: false)
expect(post).not_to be_published
end
end
end
# spec/requests/api/v1/posts_spec.rb
require 'rails_helper'
RSpec.describe 'Api::V1::Posts', type: :request do
let(:user) { create(:user) }
let(:headers) { { 'Authorization' => "Bearer #{user.api_token}" } }
describe 'GET /api/v1/posts' do
it 'returns all published posts' do
create_list(:post, 3, published: true)
create(:post, published: false)
get '/api/v1/posts'
expect(response).to have_http_status(:success)
json = JSON.parse(response.body)
expect(json.size).to eq(3)
end
it 'paginates results' do
create_list(:post, 25, published: true)
get '/api/v1/posts', params: { page: 1, per_page: 10 }
expect(response).to have_http_status(:success)
json = JSON.parse(response.body)
expect(json.size).to eq(10)
end
end
describe 'POST /api/v1/posts' do
let(:valid_params) do
{ post: { title: 'New Post', body: 'Post body', published: true } }
end
context 'with valid parameters' do
it 'creates a new post' do
expect {
post '/api/v1/posts', params: valid_params, headers: headers
}.to change(Post, :count).by(1)
expect(response).to have_http_status(:created)
end
end
context 'with invalid parameters' do
it 'returns error messages' do
invalid_params = { post: { title: '' } }
post '/api/v1/posts', params: invalid_params, headers: headers
expect(response).to have_http_status(:unprocessable_entity)
json = JSON.parse(response.body)
expect(json['errors']).to be_present
end
end
end
end
# spec/factories/posts.rb
FactoryBot.define do
factory :post do
sequence(:title) { |n| "Post Title #{n}" }
body { Faker::Lorem.paragraphs(number: 3).join("\n\n") }
published { false }
association :author, factory: :user
association :category
trait :published do
published { true }
published_at { 1.day.ago }
end
trait :with_comments do
after(:create) do |post|
create_list(:comment, 3, post: post)
end
end
end
end
# Usage
create(:post) # Draft post
create(:post, :published) # Published post
create(:post, :published, :with_comments) # Published post with 3 comments
# .env.development
DATABASE_URL=postgres://localhost/myapp_development
REDIS_URL=redis://localhost:6379/0
SIDEKIQ_WEB_USERNAME=admin
SIDEKIQ_WEB_PASSWORD=password
# Access in code
ENV['DATABASE_URL']
ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') # With default
# config/database.yml
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
url: <%= ENV['DATABASE_URL'] %>
# Edit credentials
EDITOR=vim rails credentials:edit
# View credentials
rails credentials:show
# Production credentials
EDITOR=vim rails credentials:edit --environment production
# config/credentials.yml.enc (encrypted)
stripe:
publishable_key: pk_test_xxxxx
secret_key: sk_test_xxxxx
aws:
access_key_id: AKIAIOSFODNN7EXAMPLE
secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Access in code
Rails.application.credentials.stripe[:secret_key]
Rails.application.credentials.dig(:aws, :access_key_id)
# Models
rails generate model Post title:string body:text published:boolean author:references
rails generate model Comment body:text post:references user:references
# Controllers
rails generate controller Posts index show new create edit update destroy
rails generate controller Api::V1::Posts index show create update destroy
# Migrations
rails generate migration AddSlugToPosts slug:string:uniq
rails generate migration AddCategoryToPosts category:references
rails generate migration CreateJoinTablePostsTags posts tags
# Resources (model + migration + controller + views)
rails generate resource Post title:string body:text
# Create database
rails db:create
# Run migrations
rails db:migrate
# Rollback last migration
rails db:rollback
# Rollback specific number of migrations
rails db:rollback STEP=3
# Reset database (drop, create, migrate, seed)
rails db:reset
# Seed database
rails db:seed
# Check migration status
rails db:migrate:status
# Run all tests
bundle exec rspec
# Run specific file
bundle exec rspec spec/models/post_spec.rb
# Run specific test
bundle exec rspec spec/models/post_spec.rb:15
# Run with coverage
COVERAGE=true bundle exec rspec
# Rails console
rails console
rails c
# Production console
rails console production
# Start server
rails server
rails s
# Start server on specific port
rails s -p 3001
# Fragment caching (views)
<% cache post do %>
<%= render post %>
<% end %>
# Russian doll caching (nested)
<% cache post do %>
<%= render post %>
<% cache post.comments do %>
<%= render post.comments %>
<% end %>
<% end %>
# Low-level caching
Rails.cache.fetch("user_#{user.id}_posts", expires_in: 1.hour) do
user.posts.published.to_a
end
# Cache expiration
Rails.cache.delete("user_#{user.id}_posts")
Rails.cache.clear # Clear all cache
# Model
class Post < ApplicationRecord
belongs_to :author, class_name: 'User', counter_cache: true
end
class User < ApplicationRecord
has_many :posts
end
# Migration
add_column :users, :posts_count, :integer, default: 0, null: false
# Reset counter cache
User.find_each { |u| User.reset_counters(u.id, :posts) }
# Add indexes for foreign keys
add_index :posts, :author_id
add_index :posts, :category_id
# Compound index for common queries
add_index :posts, [:author_id, :created_at]
add_index :posts, [:published, :created_at]
# Unique index
add_index :posts, :slug, unique: true
# Partial index (PostgreSQL)
execute "CREATE INDEX index_posts_published ON posts (published_at) WHERE published = true"
# Always use strong parameters
def post_params
params.require(:post).permit(:title, :body, :published, :category_id, tag_ids: [])
end
# Nested attributes
def user_params
params.require(:user).permit(
:name, :email,
addresses_attributes: [:id, :street, :city, :_destroy]
)
end
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def update?
user.admin? || record.author == user
end
def destroy?
user.admin?
end
end
# Controller
def update
@post = Post.find(params[:id])
authorize @post # Raises Pundit::NotAuthorizedError if not allowed
if @post.update(post_params)
redirect_to @post
else
render :edit
end
end
# Controller
class PostsController < ApplicationController
before_action :authenticate_user!, except: %i[index show]
def create
@post = current_user.posts.build(post_params)
# ...
end
end
# View
<% if user_signed_in? %>
<%= link_to 'New Post', new_post_path %>
<% else %>
<%= link_to 'Sign In', new_user_session_path %>
<% end %>
Quick Reference Complete - See REFERENCE.md for comprehensive details and advanced patterns