Matthew Lang avatar

Ruby on Rails

Rails is a full-stack MVC framework built on “convention over configuration.” Most of what follows applies to Rails 7/8, which leans back toward a simpler, no-build-step default stack (Hotwire, importmaps, SQLite-friendly).

Directory structure

app/
  models/        # ActiveRecord models, business logic
  controllers/   # thin, orchestrate models + render responses
  views/         # ERB templates
  jobs/          # ActiveJob background jobs
  mailers/       # ActionMailer
  channels/      # ActionCable
  javascript/    # Stimulus controllers, importmap entry points
config/
  routes.rb
  database.yml
  application.rb
db/
  migrate/
  schema.rb

Routing

# config/routes.rb
resources :posts do
  resources :comments, only: [:create, :destroy]
  member  { post :publish }   # /posts/:id/publish
  collection { get :search }  # /posts/search
end

resource :profile, only: [:show, :edit, :update] # singular, no :id

resources gives you the 7 RESTful actions: index show new create edit update destroy.

Models (ActiveRecord)

class Post < ApplicationRecord
  belongs_to :author, class_name: "User"
  has_many :comments, dependent: :destroy
  has_one :featured_image

  validates :title, presence: true, length: { maximum: 200 }
  scope :published, -> { where.not(published_at: nil) }

  before_validation :generate_slug, on: :create
end
  • Associations: belongs_to, has_many, has_one, has_and_belongs_to_many, has_many :through.
  • Validations run before save; errors collects failures.
  • Callbacks (before_save, after_commit, etc.) — useful but easy to overuse; prefer plain methods called explicitly when the logic isn’t truly “always happens on save.”
  • Concerns (app/models/concerns) share behavior across models via modules — good for genuinely shared behavior, not just a place to dump code to avoid a fat file.
  • Enums: enum :status, %i[draft published archived].

Migrations

class AddSlugToPosts < ActiveRecord::Migration[7.1]
  def change
    add_column :posts, :slug, :string
    add_index :posts, :slug, unique: true
  end
end
  • rails g migration AddSlugToPosts slug:string:index
  • rails db:migrate, rails db:rollback, rails db:migrate:status
  • Add indexes concurrently on Postgres for large tables: add_index :posts, :slug, algorithm: :concurrently (requires disable_ddl_transaction!).

Controllers

class PostsController < ApplicationController
  before_action :set_post, only: %i[show edit update destroy]

  def index
    @posts = Post.published.order(created_at: :desc)
  end

  def create
    @post = current_user.posts.new(post_params)
    if @post.save
      redirect_to @post, notice: "Post created."
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_post = @post = Post.find(params[:id])
  def post_params = params.require(:post).permit(:title, :body)
end

Keep controllers thin: fetch, authorize, delegate to the model, respond. Push real logic down into models (or a plain Ruby object under app/models/ if it doesn’t belong to one record) rather than into controllers or service-object layers.

Views

  • ERB by default: <%= %> outputs, <% %> doesn’t.
  • Layouts in app/views/layouts/application.html.erb, yield marks content insertion.
  • Partials: _form.html.erb, rendered with render "form", post: @post or render @post (uses posts/_post).
  • Helpers live in app/helpers/.

Hotwire (default frontend stack since Rails 7)

  • Turbo Drive: intercepts link/form navigation, swaps <body> without a full reload.
  • Turbo Frames: <turbo-frame id="..."> scopes navigation/updates to a page fragment.
  • Turbo Streams: server-sent <turbo-stream action="replace" target="..."> fragments for real-time or post-action updates.
  • Stimulus: lightweight JS controllers (app/javascript/controllers/) that attach behavior via data-controller attributes — no build step needed with importmaps.

Background jobs & async

class WelcomeEmailJob < ApplicationJob
  queue_as :default
  def perform(user) = UserMailer.welcome(user).deliver_now
end

WelcomeEmailJob.perform_later(user)

Rails 8 defaults to Solid Queue (DB-backed jobs), Solid Cache, and Solid Cable — no Redis required for a standard app.

Testing

  • Default: Minitest, in test/. rails test, rails test:models, etc.
  • Fixtures in test/fixtures/*.yml, or use factories (FactoryBot) if preferred.
  • System tests (test/system/) drive a real browser via Capybara.

Useful commands

Command Purpose
rails new app_name Scaffold a new app
rails server / rails s Start dev server
rails console / rails c REPL with app loaded
rails generate / rails g Generators (model, controller, migration…)
rails db:prepare Create + migrate + seed if needed
rails routes List all routes
bin/rails credentials:edit Edit encrypted credentials

Rails 8 notable defaults

  • SQLite is production-viable (with solid_* gems handling queue/cache/cable).
  • Kamal for zero-downtime Docker deploys to your own servers.
  • Thruster in front of Puma for HTTP/2, compression, and asset caching.
  • Built-in rails generate authentication scaffolds a real (bcrypt-based) auth system — no Devise needed for the common case.