| name | rails-conventions |
| description | Rails code patterns and conventions following rubocop-rails-omakase style. |
Rails 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:
render "posts/update"
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.
module Dispatchable
extend ActiveSupport::Concern
included do
scope :available, -> { where(status: "pending") }
end
class_methods do
def claim!(batch_size)
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
{ id:, slug:, doc_type: kind }
created_at&.iso8601
@setting ||= SlugSetting.active.find_by!(slug:)
def extract(document_type:, subject:, filename:)
def process!(strategy: nil)
Enum Patterns
STATUSES = %w[processed needs_review].freeze
enum :status, STATUSES.index_by(&:itself), validate: true
Scope Patterns
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
class InactiveSlug < StandardError; end
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:
.reload after operations
- Helper methods:
build_valid_email, with_stubbed_download
Naming (5-Second Rule)
If you can't understand in 5 seconds:
show_in_frame
process_stuff
fact_check_modal
_fact_frame
JavaScript & Importmap
Rails 7+ uses importmap for JS dependency management. Scope dependencies to the narrowest entrypoint.
Multiple Entrypoints
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "chartkick", to: "chartkick.js"
pin "Chart.bundle", to: "Chart.bundle.js"
import "@hotwired/turbo-rails"
import "@hotwired/stimulus"
import "controllers"
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