# DHH Ruby/Rails Patterns Reference Comprehensive code patterns extracted from 37signals' Campfire codebase and DHH's public teachings. ## Controller Patterns ### REST-Pure Controller Design DHH's controller philosophy is "fundamentalistic" about REST. Every controller maps to a resource with only the 7 standard actions. ```ruby # ✅ CORRECT: Standard REST actions only class MessagesController < ApplicationController def index; end def show; end def new; end def create; end def edit; end def update; end def destroy; end end # ❌ WRONG: Custom actions class MessagesController < ApplicationController def archive # NO def unarchive # NO def search # NO def drafts # NO end # ✅ CORRECT: New controllers for custom behavior class Messages::ArchivesController < ApplicationController def create # archives a message def destroy # unarchives a message end class Messages::DraftsController < ApplicationController def index # lists drafts end class Messages::SearchesController < ApplicationController def show # shows search results end ``` ### Controller Concerns for Shared Behavior ```ruby # app/controllers/concerns/room_scoped.rb module RoomScoped extend ActiveSupport::Concern included do before_action :set_room end private def set_room @room = Current.user.rooms.find(params[:room_id]) end end # Usage class MessagesController < ApplicationController include RoomScoped end ``` ### Complete Controller Example ```ruby class MessagesController < ApplicationController include ActiveStorage::SetCurrent, RoomScoped before_action :set_room, except: :create before_action :set_message, only: %i[ show edit update destroy ] before_action :ensure_can_administer, only: %i[ edit update destroy ] layout false, only: :index def index @messages = find_paged_messages if @messages.any? fresh_when @messages else head :no_content end end def create set_room @message = @room.messages.create_with_attachment!(message_params) @message.broadcast_create deliver_webhooks_to_bots rescue ActiveRecord::RecordNotFound render action: :room_not_found end def show end def edit end def update @message.update!(message_params) @message.broadcast_replace_to @room, :messages, target: [ @message, :presentation ], partial: "messages/presentation", attributes: { maintain_scroll: true } redirect_to room_message_url(@room, @message) end def destroy @message.destroy @message.broadcast_remove_to @room, :messages end private def set_message @message = @room.messages.find(params[:id]) end def ensure_can_administer head :forbidden unless Current.user.can_administer?(@message) end def find_paged_messages case when params[:before].present? @room.messages.with_creator.page_before(@room.messages.find(params[:before])) when params[:after].present? @room.messages.with_creator.page_after(@room.messages.find(params[:after])) else @room.messages.with_creator.last_page end end def message_params params.require(:message).permit(:body, :attachment, :client_message_id) end def deliver_webhooks_to_bots bots_eligible_for_webhook.excluding(@message.creator).each { |bot| bot.deliver_webhook_later(@message) } end def bots_eligible_for_webhook @room.direct? ? @room.users.active_bots : @message.mentionees.active_bots end end ``` ## Model Patterns ### Semantic Association Naming ```ruby class Message < ApplicationRecord # ✅ Semantic names that express domain concepts belongs_to :creator, class_name: "User" belongs_to :room has_many :mentions has_many :mentionees, through: :mentions, source: :user # ❌ Generic names belongs_to :user # Too generic - creator is clearer end class Room < ApplicationRecord has_many :memberships has_many :users, through: :memberships has_many :messages, dependent: :destroy # Semantic scope scope :direct, -> { where(direct: true) } def direct? direct end end ``` ### Scope Design ```ruby class Message < ApplicationRecord # Eager loading scopes scope :with_creator, -> { includes(:creator) } scope :with_attachments, -> { includes(attachment_attachment: :blob) } # Cursor-based pagination scopes scope :page_before, ->(cursor) { where("id < ?", cursor.id).order(id: :desc).limit(50) } scope :page_after, ->(cursor) { where("id > ?", cursor.id).order(id: :asc).limit(50) } scope :last_page, -> { order(id: :desc).limit(50) } # Status scopes as chainable lambdas scope :recent, -> { where("created_at > ?", 24.hours.ago) } scope :pinned, -> { where(pinned: true) } end ``` ### Custom Creation Methods ```ruby class Message < ApplicationRecord def self.create_with_attachment!(params) transaction do message = create!(params.except(:attachment)) message.attach_file(params[:attachment]) if params[:attachment].present? message end end def attach_file(attachment) file.attach(attachment) update!(has_attachment: true) end end ``` ### Authorization on Models ```ruby class User < ApplicationRecord def can_administer?(message) message.creator == self || admin? end def can_access?(room) rooms.include?(room) || admin? end def can_invite_to?(room) room.creator == self || admin? end end # Usage in controller def ensure_can_administer head :forbidden unless Current.user.can_administer?(@message) end ``` ### Model Broadcasting ```ruby class Message < ApplicationRecord after_create_commit :broadcast_create after_update_commit :broadcast_update after_destroy_commit :broadcast_destroy def broadcast_create broadcast_append_to room, :messages, target: "messages", partial: "messages/message" end def broadcast_update broadcast_replace_to room, :messages, target: dom_id(self, :presentation), partial: "messages/presentation" end def broadcast_destroy broadcast_remove_to room, :messages end end ``` ## Current Attributes Pattern ### Definition ```ruby # app/models/current.rb class Current < ActiveSupport::CurrentAttributes attribute :user attribute :session attribute :request_id attribute :user_agent resets { Time.zone = nil } def user=(user) super Time.zone = user&.time_zone end end ``` ### Setting in Controller ```ruby class ApplicationController < ActionController::Base before_action :set_current_attributes private def set_current_attributes Current.user = authenticate_user Current.session = session Current.request_id = request.request_id Current.user_agent = request.user_agent end end ``` ### Usage Throughout App ```ruby # In models class Message < ApplicationRecord before_create :set_creator private def set_creator self.creator ||= Current.user end end # In views <%= Current.user.name %> # In jobs class NotificationJob < ApplicationJob def perform(message) # Current is reset in jobs - pass what you need message.room.users.each { |user| notify(user, message) } end end ``` ## Ruby Idioms ### Guard Clauses Over Nested Conditionals ```ruby # ✅ Guard clauses def process_message return unless message.valid? return if message.spam? return unless Current.user.can_access?(message.room) message.deliver end # ❌ Nested conditionals def process_message if message.valid? unless message.spam? if Current.user.can_access?(message.room) message.deliver end end end end ``` ### Expression-less Case Statements ```ruby # ✅ Clean case without expression def status_class case when urgent? then "bg-red" when pending? then "bg-yellow" when completed? then "bg-green" else "bg-gray" end end # For routing/dispatch logic def find_paged_messages case when params[:before].present? messages.page_before(params[:before]) when params[:after].present? messages.page_after(params[:after]) else messages.last_page end end ``` ### Method Chaining ```ruby # ✅ Fluent, chainable API @room.messages .with_creator .with_attachments .excluding(@message.creator) .page_before(cursor) # On collections bots_eligible_for_webhook .excluding(@message.creator) .each { |bot| bot.deliver_webhook_later(@message) } ``` ### Implicit Returns ```ruby # ✅ Implicit return - the Ruby way def full_name "#{first_name} #{last_name}" end def can_administer?(message) message.creator == self || admin? end # ❌ Explicit return (only when needed for early exit) def full_name return "#{first_name} #{last_name}" # Unnecessary end ``` ## View Patterns ### Helper Methods for Complex HTML ```ruby # app/helpers/messages_helper.rb module MessagesHelper def message_container(message, &block) tag.div( id: dom_id(message), class: message_classes(message), data: { controller: "message", message_id_value: message.id, action: "click->message#select" }, &block ) end private def message_classes(message) classes = ["message"] classes << "message--mine" if message.creator == Current.user classes << "message--highlighted" if message.highlighted? classes.join(" ") end end ``` ### Turbo Frame Patterns ```erb <%# app/views/messages/index.html.erb %> <%= turbo_frame_tag "messages", data: { turbo_action: "advance" } do %> <%= render @messages %> <% if @messages.any? %> <%= link_to "Load more", room_messages_path(@room, before: @messages.last.id), data: { turbo_frame: "messages" } %> <% end %> <% end %> ``` ### Stimulus Controller Integration ```erb