About rails-conventions
A collection of opinionated Rails coding and testing conventions for developers maintaining Rails applications. It favors simple, testable structures and includes guidance on controllers, services, Turbo Streams, JavaScript imports, and performance.
Key features
- 01Guidance for controller and model boundaries
- 02Rules for extracting service objects
- 03Inline Turbo Streams patterns
- 04Minitest and fixture-based testing
- 05Scoped JavaScript imports with importmap
- 06Performance and naming practices
Use cases
- 01Structure Rails controllers and business logic
- 02Decide when to extract service objects
- 03Test behavior with fixtures and external stubs
- 04Separate public and admin JavaScript dependencies
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ majesticlabs-dev/ majestic-marketplace --skill rails-conventionsAfter installing, type/rails-conventionsin Claude Code to confirm it loaded.
Details
From the project READMERails Conventions
Opinionated Rails patterns for clean, maintainable code.
Core Philosophy
-
Duplication > Complexity: Simple duplicated code beats complex DRY abstractions
- "I'd rather have four simple controllers than three complex ones"
-
Testability = Quality: If it's hard to test, structure needs refactoring
-
Adding controllers is never bad. Making controllers complex IS bad.
Turbo Streams
Simple turbo streams MUST be inline in controllers:
# FAIL: Separate .turbo_stream.erb files for simple operations
render "posts/update"
# PASS: Inline array
render turbo_stream: [
turbo_stream.replace("post_#{@post.id}", partial: "posts/post", locals: { post: @post }),
turbo_stream.remove("flash")
]
Controller & Concerns
Business logic belongs in models or concerns, not controllers.
# Concern structure
module Dispatchable
extend ActiveSupport::Concern
included do
scope :available, -> { where(status: "pending") }
end
class_methods do
def claim!(batch_size)
# class-level behavior
end
end
end
Service Extraction
Extract when you see MULTIPLE of:
- Complex business rules (not just "it's long")
- Multiple models orchestrated
- External API interactions
- Reusable cross-controller logic
Service structure:
- Single public method
- Namespace by responsibility (
Extraction::RegexExtractor) - Constructor takes dependencies
- Return data structures, not domain objects
Modern Ruby Style
# Hash shorthand
{ id:, slug:, doc_type: kind }
# Safe navigation
created_at&.iso8601
@setting ||= SlugSetting.active.find_by!(slug:)
# Keyword arguments
def extract(document_type:, subject:, filename:)
def process!(strategy: nil)
Enum Patterns
# Frozen arrays with validation
STATUSES = %w[processed needs_review].freeze
enum :status, STATUSES.index_by(&:itself), validate: true
Scope Patterns
# Guard with .present?, chainable design
scope :by_slug, ->(slug) { where(slug:) if slug.present? }
scope :from_date, ->(date) { where(created_at: Date.parse(date).beginning_of_day..) if date.present? }
def self.filtered(params)
all.by_slug(params[:slug]).by_kind(params[:kind])
rescue ArgumentError
all
end
Error Handling
# Domain-specific errors
class InactiveSlug < StandardError; end
# Log with context, re-raise for upstream
def handle_exception!(error:)
log_error("Exception #{error.class}: #{error.message}", error:)
mark_failed!(error.message)
raise
end
Testing (Minitest + Fixtures)
test "describes expected behavior" do
email = emails(:two)
email.process
email.reload
assert_equal "finished", email.processing_status
end
Principles:
- Behavior-driven: Test what, not how
- Fixture-based: Use
emails(:two)for setup - Mock externals: Stub S3, APIs, PDFs
- State verification:
.reloadafter operations - Helper methods:
build_valid_email,with_stubbed_download
Naming (5-Second Rule)
If you can't understand in 5 seconds:
# FAIL
show_in_frame
process_stuff
# PASS
fact_check_modal
_fact_frame
JavaScript & Importmap
Rails 7+ uses importmap for JS dependency management. Scope dependencies to the narrowest entrypoint.
Multiple Entrypoints
# config/importmap.rb
pin "application" # Public entrypoint
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
# Admin-only dependencies - pinned but only imported in admin entrypoint
pin "chartkick", to: "chartkick.js"
pin "Chart.bundle", to: "Chart.bundle.js"
// app/javascript/application.js — public pages only
import "@hotwired/turbo-rails"
import "@hotwired/stimulus"
import "controllers"
// app/javascript/admin.js — imports public base + admin-only libs
import "application"
import "chartkick"
import "Chart.bundle"
<%# Admin layout %>
<%= javascript_importmap_tags("admin") %>
<%# All other layouts %>
<%= javascript_importmap_tags %>
Adding New JS Dependencies
- Determine scope: public-facing or admin-only?
- Pin in
config/importmap.rb - Import in the correct entrypoint
- Never add admin-only libraries to
application.js
Performance
- Consider scale impact
- No premature caching
- KISS - Keep It Simple
- Indexes slow writes - add only when needed