Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
sergiodxa avatar

Ruby On Rails Best Practices

  • 241 installs
  • 93 repo stars
  • Updated February 1, 2026
  • sergiodxa/agent-skills

Implement or refactor Rails APIs, models, controllers, and conventions so server-side features stay idiomatic, secure, and maintainable across releases.

About

Guides agents through Ruby on Rails backend development using framework conventions, solid ActiveRecord usage, clean controllers, and maintainable service patterns for production SaaS and API apps.

  • Rails conventions and MVC structure
  • ActiveRecord query and N+1 avoidance
  • Controller and service object patterns
  • Security and performance idioms
  • Testing-friendly backend design

Ruby On Rails Best Practices by the numbers

  • 241 all-time installs (skills.sh)
  • +6 installs in the week ending Aug 5, 2026 (Skillselion tracking)
  • Ranked #1,620 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/sergiodxa/agent-skills --skill ruby-on-rails-best-practices

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs241
repo stars93
Last updatedFebruary 1, 2026
Repositorysergiodxa/agent-skills

What it does

Implement or refactor Rails APIs, models, controllers, and conventions so server-side features stay idiomatic, secure, and maintainable across releases.

Files

SKILL.mdMarkdownGitHub ↗

Ruby on Rails Best Practices

Architecture patterns and coding conventions extracted from Basecamp's production Rails applications (Fizzy and Campfire). Contains 16 rules across 6 categories focused on code organization, maintainability, and following "The Rails Way" with Basecamp's refinements.

When to Apply

Reference these guidelines when:

  • Organizing models, concerns, and controllers
  • Writing background jobs
  • Implementing real-time features with Turbo Streams
  • Deciding where code should live
  • Writing tests for Rails applications
  • Reviewing Rails code for architectural consistency

Rules Summary

Model Organization (HIGH)

model-scoped-concerns - @rules/model-scoped-concerns.md

Place model-specific concerns in app/models/model_name/ not app/models/concerns/.

# Directory structure
app/models/
├── card.rb
├── card/
│   ├── closeable.rb     # Card::Closeable
│   ├── searchable.rb    # Card::Searchable
│   └── assignable.rb    # Card::Assignable

# app/models/card.rb
class Card < ApplicationRecord
  include Closeable, Searchable, Assignable
  # Ruby resolves from Card:: namespace first
end
concern-naming - @rules/concern-naming.md

Use -able suffix for behavior concerns, nouns for feature concerns.

# Behaviors: -able suffix
module Card::Closeable     # Can be closed
module Card::Searchable    # Can be searched
module User::Mentionable   # Can be mentioned

# Features: nouns
module User::Avatar        # Has avatar
module User::Role          # Has role
module Card::Mentions      # Has @mentions
template-method-concerns - @rules/template-method-concerns.md

Use template methods in shared concerns for customizable behavior.

# app/models/concerns/searchable.rb (shared)
module Searchable
  def search_title
    raise NotImplementedError
  end
end

# app/models/card/searchable.rb (model-specific)
module Card::Searchable
  include ::Searchable

  def search_title
    title  # Implement the hook
  end
end

Background Jobs (HIGH)

paired-async-methods - @rules/paired-async-methods.md

Pair sync methods with _later variants that enqueue jobs.

# app/models/card/readable.rb
def remove_inaccessible_notifications
  # Sync implementation
end

private
  def remove_inaccessible_notifications_later
    Card::RemoveInaccessibleNotificationsJob.perform_later(self)
  end

# app/jobs/card/remove_inaccessible_notifications_job.rb
class Card::RemoveInaccessibleNotificationsJob < ApplicationJob
  def perform(card)
    card.remove_inaccessible_notifications
  end
end
thin-jobs - @rules/thin-jobs.md

Jobs call model methods. All logic lives in models.

# Bad: Logic in job
class ProcessOrderJob < ApplicationJob
  def perform(order)
    order.items.each { |i| i.product.decrement!(:stock) }
    order.update!(status: :processing)
  end
end

# Good: Job delegates to model
class ProcessOrderJob < ApplicationJob
  def perform(order)
    order.process  # Single method call
  end
end

Controllers (HIGH)

resource-controllers - @rules/resource-controllers.md

Create resource controllers for state changes, not custom actions.

# Bad: Custom actions
resources :cards do
  post :close
  post :reopen
end

# Good: Resource controllers
resources :cards do
  resource :closure, only: [:create, :destroy]
end

# app/controllers/cards/closures_controller.rb
class Cards::ClosuresController < ApplicationController
  def create
    @card.close
  end

  def destroy
    @card.reopen
  end
end
scoping-concerns - @rules/scoping-concerns.md

Use concerns like CardScoped for nested resource setup.

# app/controllers/concerns/card_scoped.rb
module CardScoped
  extend ActiveSupport::Concern

  included do
    before_action :set_card
  end

  private
    def set_card
      @card = Current.user.accessible_cards.find_by!(number: params[:card_id])
    end
end

# Usage
class Cards::CommentsController < ApplicationController
  include CardScoped
end
thin-controllers - @rules/thin-controllers.md

Controllers call rich model APIs directly. No service objects.

# Good: Thin controller, rich model
class Cards::ClosuresController < ApplicationController
  include CardScoped

  def create
    @card.close  # All logic in model
  end
end

Request Context (MEDIUM)

current-attributes - @rules/current-attributes.md

Use Current for request-scoped data with cascading setters.

class Current < ActiveSupport::CurrentAttributes
  attribute :session, :user, :account

  def session=(value)
    super(value)
    self.user = session&.user
  end
end
current-in-other-contexts - @rules/current-in-other-contexts.md

Current is only auto-populated in web requests. Jobs, mailers, and channels need explicit setup.

# Jobs: extend ActiveJob to serialize/restore Current.account
# Mailers from jobs: wrap in Current.with_account { mailer.deliver }
# Channels: set Current in Connection#connect

Associations & Callbacks (MEDIUM)

association-extensions - @rules/association-extensions.md

Choose between association extensions and model class methods based on context needs.

# Use extension when you need parent context (proxy_association.owner)
has_many :accesses do
  def grant_to(users)
    board = proxy_association.owner
    Access.insert_all(users.map { |u| { user_id: u.id, board_id: board.id, account_id: board.account_id } })
  end
end

# Use class method when operation is independent
class Access
  def self.grant(board:, users:)
    insert_all(users.map { |u| { user_id: u.id, board_id: board.id } })
  end
end
callbacks-patterns - @rules/callbacks-patterns.md

Use after_commit for jobs, inline lambdas for simple ops.

# Jobs: after_commit
after_create_commit :notify_recipients_later

# Simple ops: inline lambda
after_save -> { board.touch }, if: :published?

# Conditional: remember and check pattern
before_update :remember_changes
after_update_commit :process_changes, if: :should_process?

Turbo & Real-time (MEDIUM)

turbo-broadcasts - @rules/turbo-broadcasts.md

Explicit broadcasts from controllers, not callbacks.

# app/models/message/broadcasts.rb
module Message::Broadcasts
  def broadcast_create
    broadcast_append_to room, :messages, target: [room, :messages]
  end
end

# Controller calls explicitly
def create
  @message = @room.messages.create!(message_params)
  @message.broadcast_create
end

Testing (MEDIUM)

fixtures-testing - @rules/fixtures-testing.md

Use fixtures, not factories. Mirror concern structure in tests.

# test/fixtures/cards.yml
logo:
  title: The logo isn't big enough
  board: writebook
  creator: david

# test/models/card/closeable_test.rb
class Card::CloseableTest < ActiveSupport::TestCase
  test "close creates closure" do
    card = cards(:logo)
    assert_difference -> { Closure.count } do
      card.close
    end
  end
end

Code Organization (LOW-MEDIUM)

nested-service-objects - @rules/nested-service-objects.md

Place service objects under model namespace, not app/services.

# Good: app/models/card/activity_spike/detector.rb
class Card::ActivitySpike::Detector
  def initialize(card)
    @card = card
  end

  def detect
    # ...
  end
end
code-style - @rules/code-style.md

Prefer expanded conditionals, order methods by invocation.

# Expanded conditionals
def find_record
  if record = find_by_id(id)
    record
  else
    NullRecord.new
  end
end

# Method ordering: caller before callees
def process
  step_one
  step_two
end

private
  def step_one; end
  def step_two; end

Philosophy

These patterns embody "Vanilla Rails" - using Rails conventions with minimal additions:

1. Rich models, thin controllers - Domain logic in models and concerns 2. No service object layer - Controllers talk to models directly 3. Co-located code - Concerns, jobs, and services near the models they serve 4. Explicit over implicit - Call broadcasts explicitly, not via callbacks 5. Convention over configuration - Follow naming patterns for predictability

Related skills

Backend & APIsbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.