Part 2: Caching Strategy for Vue + Rails API with Nginx

In Part 1, we explored the request flow between Nginx, Vue (frontend), and Rails (API backend). We also covered how Nginx routes traffic and why caching matters in such a setup.

Now in Part 2, we’ll go deeper into asset caching strategies โ€” specifically tailored for a Rails API-only backend + Vue frontend deployed with Nginx.

๐Ÿ”‘ The Core Idea

  • HTML files (like vite.html) should never be cached. They are the entry point of the SPA and change frequently.
  • Hashed assets (like /vite/index-G34XebCm.js) can be cached for 1 year safely, because the hash ensures cache-busting.
  • Non-hashed assets (images, fonts, legacy JS/CSS) should get short-term caching (e.g., 1 hour).

This split ensures fast repeat visits while avoiding stale deploys.

๐Ÿ“‚ Example: Files in public/vite/

Your build pipeline (via Vite) outputs hashed assets like:

vite/
  index-G34XebCm.js
  DuckType-CommonsRegular-CSozX1Vl.otf
  Allergens-D48ns5vN.css
  LoginModal-DR9oLFAS.js

Notice the random-looking suffixes (G34XebCm, D48ns5vN) โ€” these are hashes. They change whenever the file content changes.

โžก๏ธ That’s why they’re safe to cache for 1 year: a new deploy creates new filenames, so the browser will fetch fresh assets.

By contrast, files like:

assets/
  15_minutes.png
  Sky_background.png

do not have hashes. If you update them, the filename doesn’t change, so the browser might keep showing stale content if cached too long. These need shorter cache lifetimes.


๐Ÿ› ๏ธ Final Nginx Caching Configuration

Here’s the Nginx cache snippet tuned for your setup:

# =====================
# HTML (always no-cache)
# =====================
location = /vite.html {
    add_header Cache-Control "no-cache";
}

location ~* \.html$ {
    add_header Cache-Control "no-cache";
}

# ==============================
# Hashed Vue/Vite assets (1 year)
# ==============================
location ^~ /vite/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

# ==================================================
# Other static assets (non-hashed) - 1 hour caching
# ==================================================
location ~* \.(?:js|css|woff2?|ttf|otf|eot|jpg|jpeg|png|gif|svg|ico)$ {
    add_header Cache-Control "public, max-age=3600";
}

๐Ÿ” Explanation

  • location = /vite.html โ†’ explicitly disables caching for the SPA entry file.
  • location ~* \.html$ โ†’ covers other .html files just in case.
  • location ^~ /vite/ โ†’ everything inside /vite/ (all hashed JS/CSS/images/fonts) gets 1 year caching.
  • Final block โ†’ fallback for other static assets like /assets/*.png, with only 1-hour cache.

โš ๏ธ What Happens If We Misconfigure?

  • If you cache .html โ†’ new deploys wonโ€™t show up, users may stay stuck on the old app shell.
  • If you cache non-hashed images for 1 year โ†’ product images may stay stale even after updates.
  • If you donโ€™t use immutable on hashed assets โ†’ browsers may still revalidate unnecessarily.

๐Ÿ—๏ธ Real-World Examples

  • GitLab uses a similar strategy with hashed Webpack assets, caching them long-term via Nginx and Cloudflare.
  • Discourse does long-term caching of fingerprinted JS/CSS, but keeps HTML dynamic with no-cache.
  • Basecamp (Rails + Hotwire) fingerprints all assets, leveraging 1-year immutable caching.

These projects rely heavily on content hashing + Nginx headers โ€” exactly what we’re setting up here.

โœ… Best Practices Recap

  1. Always fingerprint (hash) assets in production builds.
  2. Cache HTML for 0 seconds, JS/CSS hashed files for 1 year.
  3. Use immutable for hashed assets.
  4. Keep non-hashed assets on short lifetimes or rename them when updated.

This ensures smooth deploys, lightning-fast repeat visits, and no stale content issues.

๐Ÿ“Œ In Part 3, we’ll go deeper into Rails + Passenger integration, showing how Rails API responses fit into this caching strategy (and what not to cache at the API layer).


Part 1: Understanding Request Flow and Caching in a Rails + Vue + Nginx Setup

Introduction

When building modern web applications, performance is a critical factor for user experience and SEO. In setups that combine Rails (for backend logic) with Vue 3 (for the frontend), and Nginx + Passenger as the web server layer, developers must understand how requests flow through the system and how caching strategies can maximize efficiency. Without a clear understanding, issues such as stale content, redundant downloads, or poor Google PageSpeed scores can creep in.

In this series, we will break down the architecture into three detailed parts. In this first part, weโ€™ll look at the basic request flow, why caching is needed, and the specific caching strategies applied for different types of assets (HTML, hashed Vue assets, images, fonts, and SEO files).

๐Ÿ”น 1. Basic Request Flow

Letโ€™s first understand how a browser request travels through our stack. In a Rails + Vue + Nginx setup, the flow is layered so that Nginx acts as the gatekeeper, serving static files directly and passing dynamic requests to Rails via Passenger. This ensures maximum efficiency.

Browser Request (user opens https://mydomain.com)
      |
      v
+-------------------------+
|        Nginx            |
| - Serves static files   |
| - Adds cache headers    |
| - Redirects HTTP โ†’ HTTPS|
+-------------------------+
      |
      |---> /public/vite/*   (hashed Vue assets: JS, CSS, images)
      |---> /public/assets/* (general static files, fonts, images)
      |---> /public/*.html   (entry files, e.g. vite.html)
      |---> /sitemap.xml, robots.txt
      |
      v
+-------------------------+
| Passenger + Rails       |
| - Handles API requests  |
| - Renders dynamic views |
| - Business logic        |
+-------------------------+
      |
      v
Browser receives response

Key takeaways:

  • Nginx is optimized for serving static files and does this without invoking Rails.
  • Hashed Vue assets live in /public/vite/ and are safe for long-term caching.
  • HTML entry files like vite.html should never be cached aggressively, as they bootstrap the application.
  • Rails only handles requests that cannot be resolved by static files (APIs, dynamic content, authentication, etc.).

๐Ÿ”น 2. Why Caching Matters

Every time a user visits your site, the browser requests resources such as JavaScript, CSS, images, and fonts. Without caching, the browser re-downloads these assets on every visit, leading to:

  • Slower page load times
  • Higher bandwidth usage
  • Poorer SEO scores (Google PageSpeed penalizes missing caching headers)
  • Increased server load

Caching helps by instructing browsers to reuse resources when possible. However, caching needs to be carefully tuned:

  • Static, versioned assets (like hashed JS files) should be cached for a long time.
  • Dynamic or frequently changing files (like HTML, sitemap.xml) should bypass cache.
  • Non-hashed assets (like assets/*.png) can be cached for a shorter duration.

๐Ÿ”น 3. Caching Strategy in Detail

1. Hashed Vue Assets (/vite/ folder)

Files built by Vite include a content hash in their filenames (e.g., index-B34XebCm.js). This ensures that when the file content changes, the filename changes as well. Browsers see this as a new resource and download it fresh. This makes it safe to cache these files aggressively:

location /vite/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

This tells browsers to cache these files for a year, and the immutable directive prevents unnecessary revalidation.

2. HTML Files (vite.html and others)

HTML files should always be fresh because they reference the latest asset filenames. If an old HTML file is cached, it might point to outdated JS or CSS, breaking the app. Therefore, HTML must always be served with no-cache:

location ~* \.html$ {
    add_header Cache-Control "no-cache";
}

This forces browsers to check the server every time before using the file.

3. Other Static Assets (images, fonts, non-hashed JS/CSS)

Some assets in /public/assets/ do not have hashed filenames (e.g., logo.png). Caching these too aggressively could cause stale content issues. A shorter cache period (like 1 hour) is a safe balance:

location ~* \.(?:js|css|woff2?|ttf|otf|eot|jpg|jpeg|png|gif|svg|ico)$ {
    expires 1h;
    add_header Cache-Control "public";
}

4. SEO Files (sitemap.xml, robots.txt)

Search engines like Google frequently re-fetch sitemap.xml and robots.txt to keep their index up-to-date. If these files are cached, crawlers may miss recent updates. To avoid this, they should always bypass cache:

location = /sitemap.xml {
    add_header Cache-Control "no-cache";
}
location = /robots.txt {
    add_header Cache-Control "no-cache";
}

๐Ÿ”น 4. Summary Diagram

The diagram below illustrates the request flow and caching rules:

Browser Request
      |
      v
+------------------+          +-------------------+
|      Nginx       |          | Passenger + Rails |
|------------------|          |-------------------|
| - Serves /vite/* |          | - Dynamic APIs    |
|   (1y immutable) |          | - Auth flows      |
| - Serves .html   |          | - Business logic  |
|   (no-cache)     |          +-------------------+
| - Serves assets/*|
|   (1h cache)     |
| - Serves SEO     |
|   (no-cache)     |
+------------------+
      |
      v
Response to Browser

Let’s bring in some real-world examples from well-known Rails projects so you can see how this fits into practice:

๐Ÿ”น Example 1: Discourse (Rails + Ember frontend, served via Nginx + Passenger)

  • Request flow:
    • Nginx serves all static JS/CSS files that are fingerprinted (application-9f2c01f2b3f.js).
    • Rails generates these during asset precompilation.
    • Fingerprinting ensures cache-busting (like our vite/index-B34XebCm.js).
  • Caching:
    • In their Nginx config, Discourse sets: location ~ ^/assets/ { expires 1y; add_header Cache-Control "public, immutable"; }
    • All .html responses (Rails views) are marked no-cache.
    • This is exactly the same principle we applied for our /vite/ folder.

๐Ÿ”น Example 2: GitLab (Rails + Vue frontend, Nginx load balancer)

  • Request flow:
    • GitLab has Vue components bundled by Webpack (similar to Vite in our case).
    • Nginx first checks /public/assets/ for compiled frontend assets.
    • If not found โ†’ request is passed to Rails via Passenger.
  • Caching:
    • GitLab sets very aggressive caching for hashed assets, because they change only when a new release is deployed: location ~ ^/assets/.*-[a-f0-9]{32}\.(js|css|png|jpg|svg)$ { expires max; add_header Cache-Control "public, immutable"; }
    • Non-hashed files (like /uploads/ user content) get shorter caching (1 hour or 1 day).
    • HTML pages rendered by Rails = no-cache.

๐Ÿ”น Example 3: Basecamp (Rails + Hotwire, Nginx + Passenger)

  • Request flow:
    • Their entrypoint is still HTML (application.html.erb) served via Rails.
    • Static assets (CSS/JS/images) precompiled into /public/assets.
    • Nginx serves these directly, without touching Rails.
  • Caching:
    • Rails generates digest-based file names (like style-4f8d9d7.css).
    • Nginx rule: location /assets { expires 1y; add_header Cache-Control "public, immutable"; }
    • Same idea: hashed = long cache, HTML = no cache.

๐Ÿ‘‰ What this shows:

  • All large Rails projects (Discourse, GitLab, Basecamp) follow the same caching pattern we’re doing:
    • HTML โ†’ no-cache
    • Hashed assets (fingerprinted by build tool) โ†’ 1 year, immutable
    • Non-hashed assets โ†’ shorter cache (1hโ€“1d)

So what we’re implementing in our setup is the industry standard. โœ…

Conclusion

In this part, we established the foundation for how requests move through Nginx, Vue, and Rails, and why caching plays such an essential role in performance and reliability. The key principles are:

  • Hashed files = cache long term
  • HTML and SEO files = never cache
  • Non-hashed static assets = short cache
  • Rails/Passenger handles only dynamic requests

In Part 2, we’ll dive deeper into writing a complete Nginx configuration for Rails + Vue, covering gzip compression, HTTP/2 optimizations, cache busting, and optional Vue Router history mode support.


The Complete Guide to Rails Database Commands: From Basics to Production

Managing databases in Rails can seem overwhelming with all the available commands. This comprehensive guide will walk you through every essential Rails database command, from basic operations to complex real-world scenarios.

Basic Database Commands

Core Database Operations

# Create the database
rails db:create

# Drop (delete) the database
rails db:drop

# Run pending migrations
rails db:migrate

# Rollback the last migration
rails db:rollback

# Rollback multiple migrations
rails db:rollback STEP=3

Schema Management

# Load current schema into database
rails db:schema:load

# Dump current database structure to schema.rb
rails db:schema:dump

# Load structure from structure.sql (for complex databases)
rails db:structure:load

# Dump database structure to structure.sql
rails db:structure:dump

Seed Data

# Run the seed file (db/seeds.rb)
rails db:seed

Combined Commands: The Powerhouses

rails db:setup

What it does: Sets up database from scratch

rails db:setup

Equivalent to:

rails db:create
rails db:schema:load  # Loads from schema.rb
rails db:seed

When to use:

  • First time setting up project on new machine
  • Fresh development environment
  • CI/CD pipeline setup

rails db:reset

What it does: Nuclear option – completely rebuilds database

rails db:reset

Equivalent to:

rails db:drop
rails db:create
rails db:schema:load
rails db:seed

When to use:

  • Development when you want clean slate
  • After major schema changes
  • When your database is corrupted

โš ๏ธ Warning: Destroys all data!

rails db:migrate:reset

What it does: Rebuilds database using migrations

rails db:migrate:reset

Equivalent to:

rails db:drop
rails db:create
rails db:migrate  # Runs all migrations from scratch

When to use:

  • Testing that migrations run cleanly
  • Debugging migration issues
  • Ensuring migration sequence works

Advanced Database Commands

Migration Management

# Rollback to specific migration
rails db:migrate:down VERSION=20240115123456

# Re-run specific migration
rails db:migrate:up VERSION=20240115123456

# Get current migration version
rails db:version

# Check migration status
rails db:migrate:status

Database Information

# Show database configuration
rails db:environment

# Validate database and pending migrations
rails db:abort_if_pending_migrations

# Check if database exists
rails db:check_protected_environments

Environment-Specific Commands

# Run commands on specific environment
rails db:create RAILS_ENV=production
rails db:migrate RAILS_ENV=staging
rails db:seed RAILS_ENV=test

Real-World Usage Scenarios

Scenario 1: New Developer Onboarding

# New developer joins the team
git clone project-repo
cd project
bundle install

# Set up database
rails db:setup

# Or if you prefer running migrations
rails db:create
rails db:migrate
rails db:seed

Scenario 2: Production Deployment

# Safe production deployment
rails db:migrate RAILS_ENV=production

# Never run these in production:
# rails db:reset        โŒ Will destroy data!
# rails db:schema:load  โŒ Will overwrite everything!

Scenario 3: Development Workflow

# Daily development cycle
git pull origin main
rails db:migrate          # Run any new migrations

# If you have conflicts or issues
rails db:rollback         # Undo last migration
# Fix migration file
rails db:migrate          # Re-run

# Major cleanup during development
rails db:reset           # Nuclear option

Scenario 4: Testing Environment

# Fast test database setup
rails db:schema:load RAILS_ENV=test

# Or use the test-specific command
rails db:test:prepare

Environment-Specific Best Practices

Development Environment

# Liberal use of reset commands
rails db:reset              # โœ… Safe to use
rails db:migrate:reset      # โœ… Safe to use
rails db:setup              # โœ… Safe for fresh start

Staging Environment

# Mirror production behavior
rails db:migrate RAILS_ENV=staging  # โœ… Recommended
rails db:seed RAILS_ENV=staging     # โœ… If needed

# Avoid
rails db:reset RAILS_ENV=staging    # โš ๏ธ Use with caution

Production Environment

# Only safe commands
rails db:migrate RAILS_ENV=production     # โœ… Safe
rails db:rollback RAILS_ENV=production    # โš ๏ธ With backup

# Never use in production
rails db:reset RAILS_ENV=production       # โŒ NEVER!
rails db:drop RAILS_ENV=production        # โŒ NEVER!
rails db:schema:load RAILS_ENV=production # โŒ NEVER!

Pro Tips and Gotchas

Migration vs Schema Loading

# For existing databases with data
rails db:migrate          # โœ… Incremental, safe

# For fresh databases
rails db:schema:load      # โœ… Faster, clean slate

Data vs Schema

Remember that some operations preserve data differently:

  • db:migrate: Preserves existing data, applies incremental changes
  • db:schema:load: Loads clean schema, no existing data
  • db:reset: Destroys everything, starts fresh

Common Workflow Commands

# The "fix everything" development combo
rails db:reset && rails db:migrate

# The "fresh start" combo  
rails db:drop db:create db:migrate db:seed

# The "production-safe" combo
rails db:migrate db:seed

Quick Reference Cheat Sheet

CommandUse CaseData SafetySpeed
db:migrateIncremental updatesโœ… SafeMedium
db:setupInitial setupโœ… Safe (new DB)Fast
db:resetClean slateโŒ Destroys allFast
db:migrate:resetTest migrationsโŒ Destroys allSlow
db:schema:loadFresh schemaโŒ No data migrationFast
db:seedAdd sample dataโœ… AdditiveFast

Conclusion

Understanding Rails database commands is crucial for efficient development and safe production deployments. Start with the basics (db:create, db:migrate, db:seed), get comfortable with the combined commands (db:setup, db:reset), and always remember the golden rule: be very careful with production databases!

The key is knowing when to use each command:

  • Development: Feel free to experiment with db:reset and friends
  • Production: Stick to db:migrate and always have backups
  • Team collaboration: Use migrations to keep everyone in sync

Remember: migrations tell the story of how your database evolved, while schema files show where you ended up. Both are important, and now you know how to use all the tools Rails gives you to manage them effectively.


Complete Guide to RSpec with Rails 7+: From Basics to Advanced Testing

RSpec is the most popular testing framework for Ruby and Rails applications. This comprehensive guide covers everything from basic RSpec syntax to advanced Rails 7+ testing patterns, with real-world examples and scenarios.

Table of Contents

  1. RSpec Basics
  2. Rails 7+ Integration
  3. Core RSpec Methods
  4. Testing Scenarios
  5. Advanced Features
  6. Best Practices

RSpec Basics

Basic Structure

require "rails_helper"

RSpec.describe Session::AppliedDiscount do
  # Test content goes here
end

Key Components:

  • require "rails_helper" – Loads Rails testing environment
  • RSpec.describe – Groups related tests
  • describe can take a class, string, or symbol

The Building Blocks

describe and context

RSpec.describe User do
  describe "#full_name" do
    context "when first and last name are present" do
      # tests here
    end

    context "when only first name is present" do
      # tests here
    end
  end

  describe ".active_users" do
    context "with active users in database" do
      # tests here
    end
  end
end

it – Individual Test Cases

it "returns the user's full name" do
  user = User.new(first_name: "John", last_name: "Doe")
  expect(user.full_name).to eq("John Doe")
end

it "handles missing last name gracefully" do
  user = User.new(first_name: "John")
  expect(user.full_name).to eq("John")
end

Core RSpec Methods

let and let!

Lazy Evaluation with let
RSpec.describe Session::Discount do
  let(:cookies) { CookiesStub.new }
  let(:code) { create_code(10) }
  let(:customer) { init_customer }
  let(:customer_code) { create_customer_code(customer) }

  it "uses lazy evaluation" do
    # code is only created when first accessed
    expect(code.amount).to eq(10)
  end
end
Immediate Evaluation with let!
let!(:user) { User.create(name: "John") }  # Created immediately
let(:profile) { user.profile }             # Created when accessed

it "has user already created" do
  expect(User.count).to eq(1)  # user already exists
end

subject

Implicit Subject
RSpec.describe User do
  let(:user_params) { { name: "John", email: "john@example.com" } }

  subject { User.new(user_params) }

  it { is_expected.to be_valid }
  it { is_expected.to respond_to(:full_name) }
end
Named Subject
describe '#initial_discount' do
  subject(:initial_discount_in_rupee) { 
    described_class.new(cookies: cookies).initial_discount_in_rupee 
  }

  it 'returns initial discount for customer' do
    accessor.set_customer_code(customer_code: customer_code)
    expect(initial_discount_in_rupee).to eq(expected_amount)
  end
end

expect and Matchers

Basic Matchers
# Equality
expect(user.name).to eq("John")
expect(user.age).to be > 18
expect(user.email).to include("@")

# Boolean checks
expect(user).to be_valid
expect(user.active?).to be true
expect(user.admin?).to be_falsy

# Type checks
expect(user.created_at).to be_a(Time)
expect(user.tags).to be_an(Array)
Collection Matchers
expect(users).to include(john_user)
expect(user.roles).to contain_exactly("admin", "user")
expect(shopping_cart.items).to be_empty
expect(search_results).to have(3).items
String Matchers
expect(user.email).to match(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
expect(response.body).to include("Welcome")
expect(error_message).to start_with("Error:")
expect(success_message).to end_with("successfully!")

Rails 7+ Integration

Rails Helper Setup

# spec/rails_helper.rb
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'

abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'

RSpec.configure do |config|
  config.fixture_path = "#{::Rails.root}/spec/fixtures"
  config.use_transactional_fixtures = true
  config.infer_spec_type_from_file_location!
  config.filter_rails_from_backtrace!
end

Testing Controllers

RSpec.describe Api::V1::SessionsController, type: :controller do
  let(:user) { create(:user) }
  let(:valid_params) { { email: user.email, password: "password" } }

  describe "POST #create" do
    context "with valid credentials" do
      it "returns success response" do
        post :create, params: valid_params
        expect(response).to have_http_status(:success)
        expect(JSON.parse(response.body)["success"]).to be true
      end

      it "sets authentication token" do
        post :create, params: valid_params
        expect(response.cookies["auth_token"]).to be_present
      end
    end

    context "with invalid credentials" do
      it "returns unauthorized status" do
        post :create, params: { email: user.email, password: "wrong" }
        expect(response).to have_http_status(:unauthorized)
      end
    end
  end
end

Testing Models

RSpec.describe User, type: :model do
  describe "validations" do
    it { is_expected.to validate_presence_of(:email) }
    it { is_expected.to validate_uniqueness_of(:email) }
    it { is_expected.to validate_length_of(:password).is_at_least(8) }
  end

  describe "associations" do
    it { is_expected.to have_many(:orders) }
    it { is_expected.to belong_to(:organization) }
    it { is_expected.to have_one(:profile) }
  end

  describe "scopes" do
    let!(:active_user) { create(:user, :active) }
    let!(:inactive_user) { create(:user, :inactive) }

    it "returns only active users" do
      expect(User.active).to include(active_user)
      expect(User.active).not_to include(inactive_user)
    end
  end
end

Testing Scenarios

Testing Service Objects

RSpec.describe Session::Discount do
  let(:cookies) { CookiesStub.new }
  let(:accessor) { Session::CookieDiscount.new(cookies) }

  describe '#initialize' do
    it 'calls ClearDiscountCode' do
      expect_any_instance_of(Session::ClearDiscountCode).to receive(:run)
      described_class.new(cookies: cookies)
    end

    it 'removes discount_code if referral_code presented' do
      accessor.set_code(discount)
      accessor.set_referral_code(referral_code: code)

      described_class.new(cookies: cookies)
      expect(accessor.discount).to be nil
    end
  end
end

Testing API Endpoints

RSpec.describe "API V1 Sessions", type: :request do
  let(:headers) { { "Content-Type" => "application/json" } }

  describe "POST /api/v1/sessions" do
    let(:user) { create(:user) }
    let(:params) do
      {
        session: {
          email: user.email,
          password: "password"
        }
      }
    end

    it "creates a new session" do
      post "/api/v1/sessions", params: params.to_json, headers: headers

      expect(response).to have_http_status(:created)
      expect(json_response["user"]["id"]).to eq(user.id)
      expect(json_response["token"]).to be_present
    end

    context "with invalid credentials" do
      before { params[:session][:password] = "wrong_password" }

      it "returns error" do
        post "/api/v1/sessions", params: params.to_json, headers: headers

        expect(response).to have_http_status(:unauthorized)
        expect(json_response["error"]).to eq("Invalid credentials")
      end
    end
  end
end

Testing Background Jobs

RSpec.describe EmailNotificationJob, type: :job do
  include ActiveJob::TestHelper

  let(:user) { create(:user) }

  describe "#perform" do
    it "sends welcome email" do
      expect {
        EmailNotificationJob.perform_now(user.id, "welcome")
      }.to change { ActionMailer::Base.deliveries.count }.by(1)
    end

    it "enqueues job" do
      expect {
        EmailNotificationJob.perform_later(user.id, "welcome")
      }.to have_enqueued_job(EmailNotificationJob)
    end
  end
end

Testing with Database Transactions

RSpec.describe OrderProcessor do
  describe "#process" do
    let(:order) { create(:order, :pending) }
    let(:payment_method) { create(:payment_method) }

    it "processes order successfully" do
      expect {
        OrderProcessor.new(order).process(payment_method)
      }.to change { order.reload.status }.from("pending").to("completed")
    end

    it "handles payment failures" do
      allow(payment_method).to receive(:charge).and_raise(PaymentError)

      expect {
        OrderProcessor.new(order).process(payment_method)
      }.to raise_error(PaymentError)

      expect(order.reload.status).to eq("failed")
    end
  end
end

Advanced Features

Shared Examples

# spec/support/shared_examples/auditable.rb
RSpec.shared_examples "auditable" do
  it "tracks creation" do
    expect(subject.created_at).to be_present
    expect(subject.created_by).to eq(current_user)
  end

  it "tracks updates" do
    subject.update(name: "Updated Name")
    expect(subject.updated_by).to eq(current_user)
  end
end

# Usage in specs
RSpec.describe User do
  let(:current_user) { create(:user) }
  subject { create(:user) }

  it_behaves_like "auditable"
end

Custom Matchers

# spec/support/matchers/be_valid_email.rb
RSpec::Matchers.define :be_valid_email do
  match do |actual|
    actual =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  end

  failure_message do |actual|
    "expected #{actual} to be a valid email address"
  end
end

# Usage
expect(user.email).to be_valid_email

Hooks and Callbacks

RSpec.describe User do
  before(:each) do
    @original_time = Time.current
    travel_to Time.zone.parse("2023-01-01 12:00:00")
  end

  after(:each) do
    travel_back
  end

  before(:all) do
    # Runs once before all tests in this describe block
    @test_data = create_test_data
  end

  around(:each) do |example|
    Rails.logger.silence do
      example.run
    end
  end
end

Stubbing and Mocking

describe "external API integration" do
  let(:api_client) { instance_double("APIClient") }

  before do
    allow(APIClient).to receive(:new).and_return(api_client)
  end

  it "calls external service" do
    expect(api_client).to receive(:get_user_data).with(user.id)
      .and_return({ name: "John", email: "john@example.com" })

    result = UserDataService.fetch(user.id)
    expect(result[:name]).to eq("John")
  end

  it "handles API errors gracefully" do
    allow(api_client).to receive(:get_user_data).and_raise(Net::TimeoutError)

    expect {
      UserDataService.fetch(user.id)
    }.to raise_error(ServiceUnavailableError)
  end
end

Testing Time-dependent Code

describe "subscription expiry" do
  let(:subscription) { create(:subscription, expires_at: 2.days.from_now) }

  it "is not expired when current" do
    expect(subscription).not_to be_expired
  end

  it "is expired when past expiry date" do
    travel_to 3.days.from_now do
      expect(subscription).to be_expired
    end
  end
end

Factory Bot Integration

Basic Factory Setup

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "user#{n}@example.com" }
    first_name { "John" }
    last_name { "Doe" }
    password { "password123" }

    trait :admin do
      role { "admin" }
    end

    trait :with_profile do
      after(:create) do |user|
        create(:profile, user: user)
      end
    end

    factory :admin_user, traits: [:admin]
  end
end

# Usage in tests
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:user_with_profile) { create(:user, :with_profile) }

Advanced Factory Patterns

# spec/factories/orders.rb
FactoryBot.define do
  factory :order do
    user
    total_amount { 100.00 }
    status { "pending" }

    factory :completed_order do
      status { "completed" }
      completed_at { Time.current }

      after(:create) do |order|
        create_list(:order_item, 3, order: order)
      end
    end
  end
end

Testing Different Types

Feature Tests (System Tests)

RSpec.describe "User Registration", type: :system do
  it "allows user to register" do
    visit "/signup"

    fill_in "Email", with: "test@example.com"
    fill_in "Password", with: "password123"
    fill_in "Confirm Password", with: "password123"

    click_button "Sign Up"

    expect(page).to have_content("Welcome!")
    expect(page).to have_current_path("/dashboard")
  end
end

Mailer Tests

RSpec.describe UserMailer, type: :mailer do
  describe "#welcome_email" do
    let(:user) { create(:user) }
    let(:mail) { UserMailer.welcome_email(user) }

    it "sends to correct recipient" do
      expect(mail.to).to eq([user.email])
    end

    it "has correct subject" do
      expect(mail.subject).to eq("Welcome to Our App!")
    end

    it "includes user name in body" do
      expect(mail.body.encoded).to include(user.first_name)
    end
  end
end

Helper Tests

RSpec.describe ApplicationHelper, type: :helper do
  describe "#format_currency" do
    it "formats positive amounts" do
      expect(helper.format_currency(100.50)).to eq("$100.50")
    end

    it "handles zero amounts" do
      expect(helper.format_currency(0)).to eq("$0.00")
    end

    it "formats negative amounts" do
      expect(helper.format_currency(-50.25)).to eq("-$50.25")
    end
  end
end

Best Practices

1. Clear Test Structure

# Good: Clear, descriptive names
describe User do
  describe "#full_name" do
    context "when both names are present" do
      it "returns concatenated first and last name" do
        # test implementation
      end
    end
  end
end

# Bad: Unclear names
describe User do
  it "works" do
    # test implementation
  end
end

2. One Assertion Per Test

# Good: Single responsibility
it "validates email presence" do
  user = User.new(email: nil)
  expect(user).not_to be_valid
end

it "validates email format" do
  user = User.new(email: "invalid-email")
  expect(user).not_to be_valid
end

# Bad: Multiple assertions
it "validates email" do
  user = User.new(email: nil)
  expect(user).not_to be_valid

  user.email = "invalid-email"
  expect(user).not_to be_valid

  user.email = "valid@email.com"
  expect(user).to be_valid
end

3. Use let for Test Data

# Good: Reusable and lazy-loaded
let(:user) { create(:user, email: "test@example.com") }
let(:order) { create(:order, user: user, total: 100) }

it "calculates tax correctly" do
  expect(order.tax_amount).to eq(8.50)
end

# Bad: Repeated setup
it "calculates tax correctly" do
  user = create(:user, email: "test@example.com")
  order = create(:order, user: user, total: 100)
  expect(order.tax_amount).to eq(8.50)
end

4. Meaningful Error Messages

# Good: Custom error messages
expect(discount.amount).to eq(50), 
  "Expected discount amount to be $50 for premium users"

# Good: Descriptive matchers
expect(user.subscription).to be_active,
  "User subscription should be active after successful payment"

5. Test Edge Cases

describe "#divide" do
  it "divides positive numbers" do
    expect(calculator.divide(10, 2)).to eq(5)
  end

  it "handles division by zero" do
    expect { calculator.divide(10, 0) }.to raise_error(ZeroDivisionError)
  end

  it "handles negative numbers" do
    expect(calculator.divide(-10, 2)).to eq(-5)
  end

  it "handles float precision" do
    expect(calculator.divide(1, 3)).to be_within(0.001).of(0.333)
  end
end

Rails 7+ Specific Features

Testing with ActionText

RSpec.describe Post, type: :model do
  describe "rich text content" do
    let(:post) { create(:post) }

    it "can store rich text content" do
      post.content = "<p>Hello <strong>world</strong></p>"
      expect(post.content.to_s).to include("Hello")
      expect(post.content.to_s).to include("<strong>world</strong>")
    end
  end
end

Testing with Active Storage

RSpec.describe User, type: :model do
  describe "avatar attachment" do
    let(:user) { create(:user) }
    let(:image) { fixture_file_upload("spec/fixtures/avatar.jpg", "image/jpeg") }

    it "can attach avatar" do
      user.avatar.attach(image)
      expect(user.avatar).to be_attached
      expect(user.avatar.content_type).to eq("image/jpeg")
    end
  end
end

Testing Hotwire/Turbo

RSpec.describe "Todo Management", type: :system do
  it "updates todo via turbo stream" do
    todo = create(:todo, title: "Original Title")

    visit todos_path
    click_link "Edit"
    fill_in "Title", with: "Updated Title"
    click_button "Update"

    expect(page).to have_content("Updated Title")
    expect(page).not_to have_content("Original Title")
    # Verify it was updated via AJAX, not full page reload
    expect(page).not_to have_selector(".flash-message")
  end
end

Configuration and Setup

RSpec Configuration

# spec/rails_helper.rb
RSpec.configure do |config|
  # Database cleaner
  config.use_transactional_fixtures = true

  # Factory Bot
  config.include FactoryBot::Syntax::Methods

  # Custom helpers
  config.include AuthenticationHelpers, type: :request
  config.include ControllerHelpers, type: :controller

  # Filtering
  config.filter_run_when_matching :focus
  config.example_status_persistence_file_path = "spec/examples.txt"

  # Parallel execution
  config.order = :random
  Kernel.srand config.seed
end

Database Cleaner Setup

# spec/rails_helper.rb
require 'database_cleaner/active_record'

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end

  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end

This comprehensive guide covers the essential RSpec patterns you’ll use in Rails 7+ applications. The examples shown are based on real-world scenarios and follow current best practices for maintainable, reliable test suites.

Remember: Good tests are documentation for your code – they should clearly express what your application does and how it should behave under different conditions.


The Complete Guide to Cookie Storage in Rails 7: Security, Performance, and Best Practices

Cookies are fundamental to web applications, but choosing the right storage method can make or break your app’s security and performance. Rails 7 offers multiple cookie storage mechanisms, each with distinct security properties and use cases. Let’s explore when to use each approach and why it matters.

The Cookie Storage Spectrum

Rails provides four main cookie storage methods, each offering different levels of security:

# 1. Plain cookies - readable and modifiable by client
cookies[:theme] = 'dark'

# 2. Signed cookies - readable but tamper-proof
cookies.signed[:discount_code] = 'SAVE10'

# 3. Encrypted cookies - hidden and tamper-proof
cookies.encrypted[:user_preferences] = { notifications: true }

# 4. Session storage - server-side with encrypted session cookie
session[:current_user_id] = user.id

1. Plain Cookies: When Transparency is Acceptable

Use for: Non-sensitive data where client-side reading/modification is acceptable or even desired.

# Setting a plain cookie
cookies[:theme] = 'dark'
cookies[:language] = 'en'
cookies[:consent_given] = 'true'

# With expiration
cookies[:temporary_banner_dismissed] = {
  value: 'true',
  expires: 1.day.from_now
}

Security implications:

  • โœ… Fast and simple
  • โŒ Completely readable in browser dev tools
  • โŒ User can modify values freely
  • โŒ No protection against tampering

Best for:

  • UI preferences (theme, language)
  • Non-critical flags (banner dismissal)
  • Data you want JavaScript to access easily

2. Signed Cookies: Tamper-Proof but Visible

Signed cookies prevent modification while remaining readable. Rails uses HMAC-SHA1 with your secret_key_base to create a cryptographic signature.

# Setting signed cookies
cookies.signed[:discount_code] = 'SAVE10'
cookies.signed[:referral_source] = 'google_ads'

# Reading signed cookies
discount = cookies.signed[:discount_code]  # Returns 'SAVE10' or nil if tampered

How it works:

# Rails internally does:
# 1. Create signature: HMAC-SHA1(secret_key_base, 'SAVE10')
# 2. Store: Base64.encode64('SAVE10--signature')
# 3. On read: verify signature matches content

Security implications:

  • โœ… Tamper-proof – modification invalidates the cookie
  • โœ… Prevents privilege escalation attacks
  • โš ๏ธ Content still visible (Base64 encoded)
  • โŒ Not suitable for truly sensitive data

Real-world example from our codebase:

# lib/session/cookie_discount_accessor.rb
def discount_code
  # Prevents users from changing 'SAVE10' to 'SAVE50' in browser
  @cookies.signed[:discount] && DiscountCode.find_by(name: @cookies.signed[:discount])
end

def set_discount_code(code)
  @cookies.signed[:discount] = {
    value: code.name,
    expires: code.expiration || 30.days.from_now
  }
end

Best for:

  • Discount codes
  • Referral tracking
  • Non-sensitive IDs that shouldn’t be modified
  • Data integrity without confidentiality requirements

3. Encrypted Cookies: Maximum Security

Encrypted cookies are both signed and encrypted, making them unreadable and tamper-proof.

# Setting encrypted cookies
cookies.encrypted[:credit_card_last4] = '4242'
cookies.encrypted[:user_preferences] = {
  notifications: true,
  marketing_emails: false
}

# Reading encrypted cookies
preferences = cookies.encrypted[:user_preferences]

Security implications:

  • โœ… Content completely hidden from client
  • โœ… Tamper-proof
  • โœ… Suitable for sensitive data
  • โš ๏ธ Slightly higher CPU overhead
  • โš ๏ธ Size limitations (4KB total per domain)

Best for:

  • Personal information
  • Financial data
  • Complex user preferences
  • Any data you’d store in a database but need client-side

4. Session Storage: Server-Side Security

Rails sessions are encrypted cookies by default, but the data is conceptually server-side.

# Session storage
session[:current_user_id] = user.id
session[:shopping_cart] = cart.to_h
session[:two_factor_verified] = true

# Configuration in config/application.rb
config.session_store :cookie_store, key: '_myapp_session'

Security implications:

  • โœ… Encrypted by default
  • โœ… Automatic expiration handling
  • โœ… CSRF protection integration
  • โš ๏ธ 4KB size limit
  • โš ๏ธ Lost on cookie deletion

Best for:

  • User authentication state
  • Shopping carts
  • Multi-step form data
  • Security-sensitive flags

Security Best Practices

1. Choose the Right Storage Method

# โŒ Don't store sensitive data in plain cookies
cookies[:ssn] = '123-45-6789'  # Visible to everyone!

# โœ… Use appropriate security level
cookies.encrypted[:ssn] = '123-45-6789'  # Hidden and protected
session[:user_id] = user.id              # Server-side, encrypted

2. Set Proper Cookie Attributes

# Secure cookies for HTTPS
cookies[:theme] = {
  value: 'dark',
  secure: Rails.env.production?,  # HTTPS only
  httponly: true,                 # No JavaScript access
  samesite: :strict              # CSRF protection
}

3. Handle Cookie Tampering Gracefully

def current_discount_code
  code_name = cookies.signed[:discount]
  return nil unless code_name

  DiscountCode.find_by(name: code_name)&.tap do |code|
    # Remove if expired or invalid
    cookies.delete(:discount) unless code.usable?
  end
end

4. Use Expiration Strategically

# Short-lived sensitive data
cookies.signed[:password_reset_token] = {
  value: token,
  expires: 15.minutes.from_now,
  secure: true,
  httponly: true
}

# Long-lived preferences
cookies.encrypted[:user_preferences] = {
  value: preferences.to_json,
  expires: 1.year.from_now
}

Advanced Patterns

1. Cookie Accessor Classes

Create dedicated classes for complex cookie management:

class Session::CookieDiscountAccessor
  def initialize(cookies)
    @cookies = cookies
  end

  def discount_code
    @cookies.signed[:discount] && DiscountCode.find_by(name: @cookies.signed[:discount])
  end

  def set_discount_code(code)
    @cookies.signed[:discount] = {
      value: code.name,
      expires: code.expiration || 30.days.from_now
    }
  end

  def remove_discount_code
    @cookies.delete(:discount)
  end
end

2. Validation and Cleanup

class Session::CheckAndRemoveDiscountCode
  def initialize(cookies:)
    @accessor = Session::CookieDiscountAccessor.new(cookies)
  end

  def run
    # Remove referral conflicts
    @accessor.referral_code && @accessor.remove_discount_code && return
      
    # Remove expired codes
    discount_code = @accessor.discount_code
    @accessor.remove_discount_code if discount_code && !discount_code.usable?
  end
end

3. Error Handling for Corrupted Cookies

def safe_read_encrypted_cookie(key)
  cookies.encrypted[key]
rescue ActiveSupport::MessageVerifier::InvalidSignature,
       ActiveSupport::MessageEncryptor::InvalidMessage
  # Cookie was corrupted or created with different secret
  cookies.delete(key)
  nil
end

Performance Considerations

Cookie Size Limits

  • Total limit: 4KB per domain
  • Individual limit: ~4KB per cookie
  • Count limit: ~50 cookies per domain

CPU Overhead

# Benchmark different storage methods
require 'benchmark'

Benchmark.bm do |x|
  x.report("plain")     { 1000.times { cookies[:test] = 'value' } }
  x.report("signed")    { 1000.times { cookies.signed[:test] = 'value' } }
  x.report("encrypted") { 1000.times { cookies.encrypted[:test] = 'value' } }
end

# Results (approximate):
#                user     system      total        real
# plain      0.001000   0.000000   0.001000 (  0.001000)
# signed     0.010000   0.000000   0.010000 (  0.009000)
# encrypted  0.050000   0.000000   0.050000 (  0.048000)

Configuration and Security Headers

Session Configuration

# config/application.rb
config.session_store :cookie_store,
  key: '_myapp_session',
  secure: Rails.env.production?,
  httponly: true,
  expire_after: 14.days,
  same_site: :lax

Security Headers

# config/application.rb
config.force_ssl = true  # HTTPS in production

# Use Secure Headers gem
SecureHeaders::Configuration.default do |config|
  config.cookies = {
    secure: true,
    httponly: true,
    samesite: {
      lax: true
    }
  }
end

Testing Cookie Security

# spec/lib/session/coupon_code_spec.rb
RSpec.describe Session::CouponCode do
  describe 'cookie tampering protection' do
    it 'handles corrupted signed cookies gracefully' do
      # Simulate tampered cookie
      cookies.signed[:discount] = 'SAVE10'
      cookies[:discount] = 'tampered_value'  # Direct manipulation

      accessor = Session::CookieDiscountAccessor.new(cookies)
      expect(accessor.discount_code).to be_nil
    end
  end
end

Migration Strategies

Upgrading Cookie Security

def upgrade_cookie_security
  # Read from old plain cookie
  if (old_value = cookies[:legacy_data])
    # Migrate to encrypted
    cookies.encrypted[:legacy_data] = old_value
    cookies.delete(:legacy_data)
  end
end

Handling Secret Key Rotation

# config/credentials.yml.enc
secret_key_base: new_secret
legacy_secret_key_base: old_secret

# In application
def read_with_fallback(key)
  cookies.encrypted[key] || begin
    # Try with old secret
    old_verifier = ActiveSupport::MessageEncryptor.new(
      Rails.application.credentials.legacy_secret_key_base
    )
    old_verifier.decrypt_and_verify(cookies[key])
  rescue
    nil
  end
end

Quick Decision Matrix

Data TypeSensitivityClient Access NeededRecommended Storage
Theme preferencesLowYesPlain cookies
Discount codesMediumNoSigned cookies
User settingsMediumNoEncrypted cookies
AuthenticationHighNoSession
Credit card dataHighNoDatabase + session ID
Shopping cartMediumNoSession or encrypted
CSRF tokensHighLimitedSession (built-in)

Common Pitfalls to Avoid

  1. Don’t mix storage types for the same data
   # โŒ Inconsistent
   cookies[:user_id] = user.id        # Sometimes
   cookies.signed[:user_id] = user.id # Other times

   # โœ… Consistent
   session[:user_id] = user.id        # Always
  1. Don’t store large objects in cookies
   # โŒ Will hit 4KB limit
   cookies.encrypted[:full_user] = user.to_json

   # โœ… Store reference
   session[:user_id] = user.id
  1. Don’t forget expiration
   # โŒ Never expires
   cookies.signed[:temp_token] = token

   # โœ… Proper expiration
   cookies.signed[:temp_token] = {
     value: token,
     expires: 1.hour.from_now
   }

Conclusion

Cookie storage in Rails 7 offers a rich toolkit for different security and performance needs. The key is matching the storage method to your data’s sensitivity and access patterns:

  • Plain cookies for non-sensitive, client-accessible data
  • Signed cookies when you need tamper protection but not confidentiality
  • Encrypted cookies for sensitive data that must remain client-side
  • Session storage for server-side state with automatic encryption

Remember: the best cookie strategy combines appropriate storage methods with proper security headers, validation, and graceful error handling. When in doubt, err on the side of more security rather than less.

The Rails cookie system is designed to make secure defaults easyโ€”take advantage of it to build applications that are both performant and secure.


Rails 7+ API error handling that scales โš–๏ธ

A solid API error strategy gives you:

  • Consistent JSON error shapes
  • Correct HTTP status codes
  • Separation of concerns (domain vs transport)
  • Observability without leaking internals

Below is a practical, production-ready approach that covers controller hooks, controllers, models/libs, background jobs, and moreโ€”illustrated with a real scenario from Session::CouponCode.

Core principles

  • Keep transport (HTTP, JSON) in controllers; keep domain logic in models/libs.
  • Map known, expected failures to specific HTTP statuses.
  • Log unexpected failures; return a generic message to clients.
  • Centralize API error rendering in a base controller.

1) A single error boundary for all API controllers

Create a base Error::ApiError and rescue it (plus a safe catchโ€‘all) in your ApiController.

# lib/error/api_error.rb
module Error
  class ApiError < StandardError
    attr_reader :status, :details
    def initialize(message, status = :unprocessable_entity, details: nil)
      super(message)
      @status  = status
      @details = details
    end
  end
end
# app/controllers/api_controller.rb
class ApiController < ActionController::Base
  include LocaleConcern
  skip_forgery_protection

  impersonates :user,
               ......

  # Specific handlers first
  rescue_from Error::ApiError,                          with: :handle_api_error
  rescue_from ActionController::ParameterMissing,       with: :handle_bad_request
  rescue_from ActiveRecord::RecordNotFound,             with: :handle_not_found
  rescue_from ActiveRecord::RecordInvalid,              with: :handle_unprocessable
  rescue_from ActiveRecord::RecordNotUnique,            with: :handle_conflict

  # Catchโ€‘all last
  rescue_from StandardError,                            with: :handle_standard_error

  private

  def handle_api_error(e)
    render json: { success: false, error: e.message, details: e.details }, status: e.status
  end

  def handle_bad_request(e)
    render json: { success: false, error: e.message }, status: :bad_request
  end

  def handle_not_found(_e)
    render json: { success: false, error: 'Not found' }, status: :not_found
  end

  def handle_unprocessable(e)
    render json: { success: false, error: e.record.errors.full_messages }, status: :unprocessable_entity
  end

  def handle_conflict(_e)
    render json: { success: false, error: 'Conflict' }, status: :conflict
  end

  def handle_standard_error(e)
    Rollbar.error(e, path: request.fullpath, client_id: try(:current_client)&.id)
    render json: { success: false, error: 'Something went wrong' }, status: :internal_server_error
  end
end
  • Order matters. Specific rescue_from before StandardError.
  • This pattern avoids duplicating rescue_from across controllers and keeps HTML controllers unaffected.

2) Errors in before actions

Because before_action runs inside controllers, the same rescue_from handlers apply.

Two patterns:

  • Render in the hook for simple guard clauses:
before_action :require_current_client

def require_current_client
  return if current_client
  render json: { success: false, error: 'require_login' }, status: :unauthorized
end
  • Raise a domain/auth error and let rescue_from handle JSON:
# lib/error/unauthorized_error.rb
module Error
  class UnauthorizedError < Error::ApiError
    def initialize(message = 'require_login') = super(message, :unauthorized)
  end
end

before_action :require_current_client

def require_current_client
  raise Error::UnauthorizedError unless current_client
end

Prefer raising if you want consistent global handling and logging.

3) Errors inside controllers

Use explicit renders for happy-path control flow; raise for domain failures:

def create
  form = CreateThingForm.new(params.require(:thing).permit(:name))
  result = CreateThing.new(form: form).call

  if result.success?
    render json: { success: true, thing: result.thing }, status: :created
  else
    # Known domain failure โ†’ raise an ApiError to map to 422
    raise Error::ApiError.new(result.message, :unprocessable_entity, details: result.details)
  end
end

Common controller exceptions (auto-mapped above):

  • ActionController::ParameterMissing โ†’ 400
  • ActiveRecord::RecordNotFound โ†’ 404
  • ActiveRecord::RecordInvalid โ†’ 422
  • ActiveRecord::RecordNotUnique โ†’ 409

4) Errors in models, services, and libs

Do not call render here. Either:

  • Return a result object (Success/Failure), or
  • Raise a domainโ€‘specific exception that the controller maps to an HTTP response.

Example from our scenario, Session::CouponCode:

# lib/error/session/coupon_code_error.rb
module Error
  module Session
    class CouponCodeError < Error::ApiError; end
  end
end
# lib/session/coupon_code.rb
class Session::CouponCode
  def discount_dollars
    # ...
    case
    when coupon_code.gift_card?
      # ...
    when coupon_code.discount_code?
      # ...
    when coupon_code.multiorder_discount_code?
      # ...
    else
      raise Error::Session::CouponCodeError, 'Unrecognized discount code'
    end
  end
end

Then, in ApiController, the specific handler (or the Error::ApiError handler) renders JSON with a 422.

This preserves separation: models/libs raise; controllers decide HTTP.

5) Other important surfaces

  • ActiveJob / Sidekiq
  • Prefer retry_on, discard_on, and jobโ€‘level rescue with logging.
  • Return no HTTP here; jobs are async.
class MyJob < ApplicationJob
  retry_on Net::OpenTimeout, wait: 10.seconds, attempts: 3
  discard_on Error::ApiError
  rescue_from(StandardError) { |e| Rollbar.error(e) }
end
  • Mailers
  • Use rescue_from to avoid bubbleโ€‘ups crashing deliveries:
class ApplicationMailer < ActionMailer::Base
  rescue_from Postmark::InactiveRecipientError, Postmark::InvalidEmailRequestError do
    # no-op / log
  end
end
  • Routing / 404
  • For APIs, keep 404 mapping at the controller boundary with rescue_from ActiveRecord::RecordNotFound.
  • For HTML, config.exceptions_app = routes + ErrorsController.
  • Middleware / Rack
  • For truly global concerns, use middleware. This is rarely necessary for controller-scoped API errors in Rails.
  • Validation vs. Exceptions
  • Use validations (ActiveModel/ActiveRecord) for expected user errors.
  • Raise exceptions for exceptional conditions (invariants violated, external systems fail unexpectedly).

6) Observability

  • Always log unexpected errors in the catchโ€‘all (StandardError).
  • Add minimal context: client_id, request.fullpath, feature flags.
  • Avoid leaking stack traces or internal messages to clients. Send generic messages on 500s.

7) Testing

  • Unit test domain services to ensure they raise Error::ApiError (or return Failure).
  • Controller/request specs: assert status codes and JSON shapes for both happy path and error path.
  • Ensure before_action guards either render or raise as intended.

Applying this to our scenario

  • /lib/session/coupon_code.rb raises Error::Session::CouponCodeError on unknown/invalid discount values.
  • /app/controllers/api_controller.rb rescues that error and returns JSON:
  • { success: false, error: e.message } with a 422 (or via Error::ApiError base).

This converts prior 500s into clean API responses and keeps error handling centralized.

When to generalize vs. specialize

  • Keep a catchโ€‘all rescue_from StandardError in ApiController to prevent 500s from leaking internals.
  • Still add specific handlers (or subclass Error::ApiError) for known cases to control the correct status code and message.
  • Do not replace everything with only StandardErrorโ€”you’ll lose semantics and proper HTTP codes.

โ€”

  • Key takeaways
  • Centralize APIโ€wide error handling in ApiController using specific handlers + a safe catchโ€‘all.
  • Raise domain errors in models/libs; render JSON only in controllers.
  • Map common Rails exceptions to correct HTTP statuses; log unexpected errors.
  • Prefer Error::ApiError as a base for consistent message/status handling across the API.

๐Ÿ”ฎ The Future of Ruby: Is It Still Relevant in 2025 and Beyond?

Ruby, the language that brought joy back into programming, is now over two decades old. It revolutionized web development through Rails and championed a developer-first philosophy. But in the era of AI, server-less, and systems programming, is Ruby still relevant? With Python dominating AI, Go owning the backend space, and Elixir praised for concurrency โ€” where does Ruby stand?

Let’s explore Ruby’s current state, the challenges it faces, and what the future might hold.


๐Ÿงฑ What Ruby Still Does Exceptionally Well

1. Web Development with Rails

Ruby on Rails remains one of the fastest and most pleasant ways to build web applications. Itโ€™s productive, expressive, and mature.

  • Companies like GitHub, Shopify, Basecamp, and Hey.com still use Rails at scale.
  • Rails 8 introduced modern features like Turbo, Hotwire, and Kamal (for zero-downtime deploys).
  • It’s still a top pick for startups wanting to build MVPs quickly.

2. Developer Happiness

The principle of “developer happiness” is deeply embedded in Ruby’s philosophy:

  • Intuitive syntax
  • Expressive and readable code
  • A community that values elegance over boilerplate

Ruby continues to be one of the best languages for teaching programming, prototyping ideas, or building software that feels joyful to write.


โš ๏ธ Challenges Facing Ruby Today

1. Performance Limitations

Rubyโ€™s performance has improved dramatically with YJIT, MJIT, and better memory handling. But it still lags behind languages like Go or Rust in raw speed, especially in CPU-bound or concurrent environments.

2. Concurrency and Parallelism

  • Ruby has a Global Interpreter Lock (GIL) in MRI, which limits real parallelism.
  • While Fibers and async gems (async, polyphony, concurrent-ruby) help, itโ€™s not as seamless as Goโ€™s goroutines or Elixirโ€™s lightweight processes.

3. Ecosystem Narrowness

Rubyโ€™s ecosystem is tightly tied to Rails.

  • Unlike Python, which powers AI, data science, and automationโ€ฆ
  • Or JavaScript, which rules the browser and serverless spaceโ€ฆ

Ruby hasnโ€™t made significant inroads outside web development.

4. Enterprise Perception

Many large enterprises shy away from Ruby, viewing it as either:

  • A “legacy startup language“, or
  • Too dynamic and flexible for highly-regulated or enterprise-scale environments.

๐Ÿ› ๏ธ How Can Ruby Improve?

๐Ÿ’ก 1. Concurrency and Async Programming

  • Embrace the shift toward non-blocking IO, async/await patterns.
  • Invest in the ecosystem around async, falcon, and evented web servers.

๐Ÿ’ก 2. AI/ML Integration

  • Ruby doesn’t need to compete with Python in AI, but it can bridge to Python using gems like pycall, pybind11, or ruby-dlib.
  • Better interop with other platforms like JRuby, TruffleRuby, or even WebAssembly can unlock new domains.

๐Ÿ’ก 3. Broaden Ecosystem Use

  • Encourage usage outside web: CLI tools, static site generation, scripting, DevOps, etc.
  • Frameworks like Hanami, Roda, Dry-rb, and Trailblazer are promising.

๐Ÿ’ก 4. Stronger Developer Outreach

  • More documentation, YouTube tutorials, free courses, and evangelism.
  • Encourage open source contribution in tools beyond Rails.

๐Ÿ“‰ Will Rails Usage Decline?

Not disappear, but become more specialized.

Rails is no longer the hottest framework โ€” but it’s still one of the most productive and complete options for web development.

  • Startups love it for speed of development.
  • Mid-sized businesses rely on it for stability and maintainability.
  • But serverless-first, JavaScript-heavy, or cloud-native stacks may bypass it in favor of Next.js, Go, or Elixir/Phoenix.

The challenge is staying competitive in the face of frameworks that promise better real-time capabilities and lightweight microservices.

๐ŸŒŸ Why Ruby Still Matters

Despite all that, Ruby still offers:

  • ๐Ÿง˜โ€โ™‚๏ธ Developer productivity
  • ๐Ÿงฉ Readable, expressive syntax
  • ๐Ÿš€ Fast prototyping
  • โค๏ธ A helpful, mature community
  • ๐Ÿงช First-class TDD culture

It’s a joy to write in Ruby. For many developers, that alone is enough.


๐Ÿ”š Final Thoughts: The Joyful Underdog

Ruby is no longer the main character in the programming language race. But that’s okay.

In a world chasing performance benchmarks, Ruby quietly reminds us: “Programming can still be beautiful.

The future of Ruby lies in:

  • Focusing on what it does best (developer experience, productivity)
  • Expanding into new areas (concurrency, scripting, interop)
  • And adapting โ€” not by competing with Go or Python, but by embracing its unique strengths.

Go with Ruby! ๐Ÿš€

Introduction to Software Development Methodologies ๐Ÿ“Š: Part 1

Software development is not just about writing code; it’s about building high-quality, maintainable, and scalable systems that deliver value to users. To achieve this consistently, teams follow structured approaches known as software development methodologies. These methodologies provide a roadmap for planning, designing, developing, testing, and delivering software.

In this three-part blog series, we’ll explore key methodologies and best practices in software development, using Ruby and Ruby on Rails examples wherever appropriate.

๐ŸŒ What Are Software Development Methodologies?

Software development methodologies are structured processes or frameworks that guide the planning and execution of software projects. They help teams manage complexity, collaborate effectively, reduce risk, and deliver projects on time.

Common Goals of Any Methodology:

  • Define clear project scope and goals
  • Break down work into manageable tasks
  • Encourage communication among team members
  • Track progress and measure success
  • Deliver working software iteratively or incrementally

๐Ÿ’ผ Why Methodologies Matter

Without a methodology, software projects often suffer from unclear requirements, missed deadlines, buggy releases, or scope creep. A good methodology:

  • Increases team productivity
  • Ensures better quality and maintainability
  • Reduces time-to-market
  • Improves customer satisfaction

In Ruby and Rails projects, where rapid development is a key feature, following a methodology keeps things under control and makes collaboration more effective.

๐Ÿ“– A Brief Overview of Popular Software Development Methodologies

We’ll explore these in detail in Part 2, but here are the major ones:

1. Waterfall

A linear approach where each phase (Requirements โ†’ Design โ†’ Implementation โ†’ Testing โ†’ Deployment) happens one after another.

2. Agile

An iterative and incremental model that encourages adaptive planning, early delivery, and continuous improvement.

3. Scrum

A popular Agile framework that structures work in sprints and emphasizes team roles, ceremonies, and artifacts.

4. Kanban

A flow-based Agile method that uses visual boards to manage work and improve process efficiency.

5. Extreme Programming (XP)

Focuses on engineering excellence, including practices like pair programming, TDD, and continuous integration. Ruby and Rails communities highly embrace XP.

6. DevOps

Combines software development and IT operations to shorten the development lifecycle and deliver high-quality software continuously.

โœจ Ruby On Rails: A Natural Fit for Agile and XP

Ruby and Rails were built with developer happiness and productivity in mind. They naturally complement Agile and XP methodologies.

Example:

  • Ruby on Rails encourages convention over configuration, which speeds up development.
  • It supports Test-Driven Development (TDD) out of the box via tools like RSpec and Minitest.
  • Features like scaffolding align well with the iterative nature of Agile.

๐Ÿ”น Coming Up in Part 2

In the next part of this series, I’ll do a deep dive into each of the above methodologies, with pros, cons, and real-world use cases. I’ll also show examples of how Rails developers apply these in practice.


Stay tuned! ๐Ÿš€

Rails 8 + ActiveAdmin: The Propshaft vs Sprockets Dilemma ๐Ÿ˜ตโ€๐Ÿ’ซ

ActiveAdmin traditionally requires Sprockets, but there are actually several alternatives and modern approaches. Let me break down your options:

ActiveAdmin Asset Pipeline Options:

Current Status (2025):

  • ActiveAdmin 3.x still primarily designed for Sprockets
  • It has SCSS files with imports, mixins, and variables that need preprocessing
  • The gem’s CSS compilation expects Sprockets’ asset compilation pipeline

Alternative Approaches:

1. Keep Sprockets (Current Choice)

Pros:

  • โœ… Fully supported, zero configuration issues
  • โœ… All ActiveAdmin features work out-of-the-box
  • โœ… Stable and well-tested

Cons:

  • โŒ Older, slower asset pipeline
  • โŒ More complex than Propshaft
  • โŒ Goes against Rails 8 defaults

2. Hybrid: Propshaft + Manual CSS for ActiveAdmin

We could try keeping Propshaft for our main app and handling ActiveAdmin CSS manually:

ActiveAdmin traditionally requires Sprockets, but the landscape is evolving. Here are your realistic options in 2025:

Current Situation:

ActiveAdmin 3.x: Requires Sprockets (stable)
ActiveAdmin 4.0 beta: Propshaft support is experimental/problematic
Rails 8 default: ๐Ÿ†• Propshaft (faster, simpler)

https://github.com/activeadmin/activeadmin/releases

Our Options:

Option 1: Stay with Sprockets โญ (Recommended for now)

Pros:

  • โœ… Zero configuration issues
  • โœ… All ActiveAdmin features work perfectly
  • โœ… Battle-tested and stable

Cons:

  • โŒ Slower than Propshaft
  • โŒ More complex asset pipeline
  • โŒ Goes against Rails 8 defaults

Option 2: ActiveAdmin Assets Gem (Simplest alternative)

The activeadmin_assets gem was mentioned as a simple solution! Let me add this option:

# Add to Gemfile
gem 'activeadmin_assets'

Pros:

  • โœ… Zero Node.js dependency
  • โœ… Works with Propshaft
  • โœ… Pre-compiled ActiveAdmin assets
  • โœ… Simple one-line installation

Cons:

  • โŒ Can’t customize ActiveAdmin styles
  • โŒ Community-maintained (not official)

Option 3: ActiveAdmin 4.0 Beta + Propshaft โš ๏ธ (Experimental)

Based on the GitHub discussions, this is possible but complex:

# Rails 8 setup with Propshaft + ActiveAdmin v4 beta
rails new myapp -a propshaft
bundle add activeadmin -v=4.0.0beta15
bundle add cssbundling-rails -v "1.4.1"
bundle add tailwindcss-ruby -v "~> 3.4.17"
yarn add tailwindcss@^3.4.17  # ActiveAdmin uses TailwindCSS v3
rails generate active_admin:install
rails css:install:tailwind
yarn add @activeadmin/activeadmin@4.0.0-beta15
npm pkg set scripts.build:css="tailwindcss -i ./app/assets/stylesheets/active_admin.css -o ./app/assets/builds/active_admin.css --minify -c tailwind-active_admin.config.js"
./bin/dev  # Instead of rails server

Pros:

  • โœ… Modern asset pipeline (Propshaft)
  • โœ… ActiveAdmin v4 features

Cons:

  • โŒ Complex setup with many steps
  • โŒ Requires Node.js/Yarn (defeats the purpose)
  • โŒ Beta software (unstable)
  • โŒ TailwindCSS v3/v4 compatibility issues

Option 4: ๐Ÿ”„ Alternative Admin Gems

If you want to avoid the ActiveAdmin complexity entirely:

RailsAdmin:

gem 'rails_admin'
  • โœ… No asset pipeline issues
  • โœ… Works with Propshaft out of the box
  • โŒ Less customizable than ActiveAdmin

Trestle:

gem 'trestle'
  • โœ… Modern Bootstrap-based admin
  • โœ… Better Rails 8 compatibility
  • โŒ Smaller community

Adminterface (Deprecated):

  • ๐Ÿšซ No longer maintained (as shown in search results)

๐Ÿ† My Recommendation:

For our production app: Stick with Sprockets + ActiveAdmin 3.x

Why?

  1. Stability – Battle-tested combination
  2. Zero configuration issues
  3. Full feature set available
  4. Easy maintenance

The performance difference between Propshaft and Sprockets won’t significantly impact your admin interface, which is typically used by a small number of internal users.

๐Ÿ”ฎ Future Outlook:

  • ActiveAdmin v4 will eventually have better Propshaft support
  • Rails community is working on solutions
  • Node.js dependency trend for complex CSS is becoming standard anyway

Bottom Line: Don’t let the asset pipeline take over the application preferences. Our current Sprockets setup is perfectly fine for an admin interface! ๐ŸŽฏ

The Reality Check:

We completely replaced Propshaft with Sprockets for the entire application.

Current State:

  • โŒ Propshaft: Completely removed
  • โœ… Sprockets: Handles ALL assets (main app + admin)
  • โœ… Importmaps: Still working (unaffected)
  • โœ… Hotwire/Turbo: Still working (unaffected)

๐Ÿ—๏ธ How ActiveAdmin Works:

ActiveAdmin is NOT a Rails Engine. It integrates directly into your app:

  1. Routes: ActiveAdmin.routes(self) adds /admin routes to your app
  2. Assets: ActiveAdmin’s CSS/JS becomes part of your asset pipeline
  3. Controllers: ActiveAdmin generates controllers in your app’s namespace

It’s tightly coupled to your application, not isolated.

๐Ÿคทโ€โ™‚๏ธ So What Are Our Real Options?

Option 1: Accept Current Setup

What we have:

  • Modern Rails 8 with Importmaps + Hotwire โœ…
  • Sprockets instead of Propshaft โŒ (but works fine)
  • Full-featured admin interface โœ…

Option 2: Use activeadmin_assets Gem

# Replace current ActiveAdmin setup
gem 'activeadmin_assets'  # Pre-compiled assets
gem 'propshaft'           # Back to Rails 8 default
  • โœ… Get Propshaft back
  • โœ… Keep Rails 8 defaults
  • โŒ Can’t customize admin styles

Option 3: Build Custom Admin

Keep your Rails 8 purity but build admin from scratch:

  • โœ… Pure Rails 8
  • โŒ Weeks of development time

๐Ÿ” What activeadmin_assets Actually Does:

The activeadmin_assets gem doesn’t replace ActiveAdmin – it provides pre-compiled static assets for ActiveAdmin to work with Propshaft.

But here’s the critical issue for you:

๐Ÿšจ activeadmin_assets = NO Customization

If you use activeadmin_assets, you get:

  • โœ… ActiveAdmin functionality
  • โœ… Propshaft compatibility
  • โŒ ZERO ability to customize styles/interface

The assets are pre-compiled and frozen – you can’t modify colors, layouts, or styling.

Since We Want Customization:

We’re actually in the BEST setup possible right now!

Our Current Setup Gives You:

  • โœ… Full ActiveAdmin customization (styles, layouts, themes)
  • โœ… Modern Rails 8 (Importmaps, Hotwire, Turbo)
  • โœ… All ActiveAdmin features (filters, actions, forms)
  • โŒ Sprockets instead of Propshaft (minor trade-off)

Customization Examples We Can Do:

  • Custom CSS themes and colors
  • Layout modifications
  • Custom JavaScript behaviors
  • Brand styling and logos
  • Advanced form customizations

๐ŸŽฏ Reality Check:

There’s no solution that gives us:

  1. Full ActiveAdmin customization AND
  2. Propshaft AND
  3. Zero complexity

We have to pick 2 out of 3.

๐Ÿ’ก My Honest Last Decision:

Keep our current setup! We’re already using:

  • Modern Rails 8 patterns (95% of your codebase)
  • Importmaps and Hotwire (exactly as intended)
  • Sprockets only affects asset serving (invisible to users)

The Propshaft vs Sprockets difference won’t affect your product’s success, but losing admin customization will limit our UX.


Guide: Integrating React โš›๏ธ into a Railsย 8 Application โ€“ Partย 4 | Synthetic Event System | Event Methods | Props

This is about React’s Synthetic Event System and naming conventions. Let me explain why React uses onClick instead of click.

React’s Synthetic Event System

React doesn’t use native DOM events directly. Instead, it creates its own Synthetic Events system that wraps around native DOM events.

HTML vs React Comparison:

<!-- Regular HTML -->
<button onclick="myFunction()">Click me</button>

<!-- React JSX -->
<button onClick={myFunction}>Click me</button>

Key Differences:

1. Naming Convention – camelCase vs lowercase:

// โŒ HTML style (lowercase) - doesn't work in React
<button click={addTodo}>Add Todo</button>
<input change={handleChange} />
<form submit={handleSubmit}>

// โœ… React style (camelCase) - correct
<button onClick={addTodo}>Add Todo</button>
<input onChange={handleChange} />
<form onSubmit={handleSubmit}>

2. More Event Examples:

// HTML โ†’ React
onclick    โ†’ onClick
onchange   โ†’ onChange
onsubmit   โ†’ onSubmit
onkeydown  โ†’ onKeyDown
onkeyup    โ†’ onKeyUp
onmouseenter โ†’ onMouseEnter
onmouseleave โ†’ onMouseLeave
onfocus    โ†’ onFocus
onblur     โ†’ onBlur

โ“ Why React Uses Synthetic Events?

1. Cross-Browser Compatibility:

// Without React - you'd need to handle browser differences
function addEventListenerCrossBrowser(element, event, handler) {
  if (element.addEventListener) {
    element.addEventListener(event, handler); // Modern browsers
  } else if (element.attachEvent) {
    element.attachEvent('on' + event, handler); // Old IE
  }
}

// With React - it handles all browser differences for you
<button onClick={addTodo}>Add Todo</button>

2. Consistent API:

// React events work the same across all browsers
const handleClick = (e) => {
  e.preventDefault();    // Always works
  e.stopPropagation();   // Always works
  console.log(e.target); // Always works
};

3. Performance Optimization:

// React uses event delegation - attaches one listener to document
// and manages all events from there (more efficient)
<div>
  <button onClick={handleClick1}>Button 1</button>
  <button onClick={handleClick2}>Button 2</button>
  <button onClick={handleClick3}>Button 3</button>
  {/* React doesn't attach 3 separate listeners */}
</div>

What happens behind the scenes:

When you write:
<button onClick={addTodo}>Add Todo</button>
React internally:
  1. Wraps the native click event in a SyntheticEvent
  2. Normalizes the event across different browsers
  3. Delegates the event handling to the document root
  4. Calls your addTodo function with the synthetic event

Example of React’s event object:

const handleClick = (e) => {
  console.log(e.type);        // "click"
  console.log(e.target);      // The button element
  console.log(e.currentTarget); // The button element
  console.log(e.preventDefault); // Function to prevent default

  // React provides the native event if needed
  console.log(e.nativeEvent); // Original DOM event
};

๐Ÿ’ก Common Mistakes:

โŒ Don’t do this:
// Wrong - lowercase (HTML style)
<button onclick={addTodo}>Add Todo</button>

// Wrong - calling the function immediately
<button onClick={addTodo()}>Add Todo</button>

// Wrong - using string (HTML style)
<button onClick="addTodo()">Add Todo</button>
โœ… Do this:
// Correct - camelCase and function reference
<button onClick={addTodo}>Add Todo</button>

// Correct - arrow function for parameters
<button onClick={() => deleteTodo(id)}>Delete</button>

// Correct - arrow function for event access
<button onClick={(e) => handleClick(e, id)}>Click</button>

Full Event System Examples:

const MyComponent = () => {
  const handleClick = (e) => {
    console.log('Button clicked!');
    e.preventDefault(); // Prevent default behavior
  };

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') {
      console.log('Enter pressed!');
    }
  };

  const handleMouseEnter = () => {
    console.log('Mouse entered!');
  };

  return (
    <div>
      <button onClick={handleClick}>Click me</button>
      <input onKeyPress={handleKeyPress} />
      <div onMouseEnter={handleMouseEnter}>Hover me</div>
    </div>
  );
};

๐Ÿ“ Summary:

React uses onClick (camelCase) instead of click (lowercase) because:

  1. Consistency – All React props use camelCase
  2. Synthetic Events – React wraps native events for better cross-browser support
  3. Performance – React optimizes event handling with event delegation
  4. Predictability – Same API across all browsers and components

The onClick you use in React is actually more powerful and reliable than the native onclick in HTML!

โ“Questions

1. How Event Delegation Improves Performance?

Event delegation is a powerful optimization technique. Let me show you the difference:

Without Event Delegation (Traditional DOM):

// Imagine you have 1000 buttons - traditional approach
const buttons = document.querySelectorAll('button');
buttons.forEach(button => {
  button.addEventListener('click', handleClick); // 1000 event listeners!
});

With Event Delegation (React’s approach):

// React attaches ONE listener to the document root
document.addEventListener('click', (e) => {
  if (e.target.matches('button')) {
    handleClick(e); // Only one listener for all buttons!
  }
});

Performance Benefits:

Memory Usage:

// Without delegation: 1000 buttons = 1000 event listeners in memory
// With delegation: 1000 buttons = 1 event listener in memory

DOM Manipulation:

// Without delegation: Adding/removing elements requires managing listeners
const newButton = document.createElement('button');
newButton.addEventListener('click', handleClick); // Must remember to add
document.body.appendChild(newButton);

// With delegation: New elements automatically work
const newButton = document.createElement('button');
document.body.appendChild(newButton); // Automatically handles clicks!

Real React Example:

// Even with 1000 todos, React only has ONE click listener
<ul>
  {todos.map(todo => (
    <li key={todo.id}>
      <button onClick={() => deleteTodo(todo.id)}>Delete</button>
      <button onClick={() => editTodo(todo.id)}>Edit</button>
    </li>
  ))}
</ul>
// React manages all these clicks with ONE delegated listener

2. Event Parameter Naming – why always use ‘e’ for events?

We can absolutely use different names! e is just a convention, not a requirement.

All of these are valid:

// Common conventions
<button onClick={(e) => handleClick(e, id)}>Click</button>
<button onClick={(event) => handleClick(event, id)}>Click</button>
<button onClick={(evt) => handleClick(evt, id)}>Click</button>

// Your custom names
<button onClick={(v) => handleClick(v, id)}>Click</button>
<button onClick={(ev) => handleClick(ev, id)}>Click</button>
<button onClick={(clickEvent) => handleClick(clickEvent, id)}>Click</button>

Why developers use ‘e’:

// 'e' is short and widely recognized
const handleClick = (e) => {
  console.log(e.target);
};

// 'event' is more descriptive but longer
const handleClick = (event) => {
  console.log(event.target);
};

// Custom names work but might confuse other developers
const handleClick = (banana) => {
  console.log(banana.target); // Works but confusing!
};

Best Practice:

// Use 'e' for short inline handlers
<button onClick={(e) => e.preventDefault()}>Click</button>

// Use 'event' for longer, more complex handlers
const handleSubmit = (event) => {
  event.preventDefault();
  event.stopPropagation();
  // ... more logic
};

3. Event Methods and Properties Explained

Explain preventDefault, stopPropagation, currentTarget, target, type etc.

Let me create a comprehensive example:

const EventDemo = () => {
  const handleClick = (e) => {
    console.log('=== EVENT PROPERTIES ===');
    console.log('e.type:', e.type);                    // "click"
    console.log('e.target:', e.target);                // Element that triggered event
    console.log('e.currentTarget:', e.currentTarget);  // Element with event listener

    console.log('=== EVENT METHODS ===');
    // e.preventDefault(); // Prevents default behavior
    // e.stopPropagation(); // Stops event from bubbling up
  };

  return (
    <div onClick={handleClick} style={{padding: '20px', border: '1px solid blue'}}>
      <h3>Parent Div</h3>
      <button onClick={handleClick}>Child Button</button>
    </div>
  );
};

e.target vs e.currentTarget:

const Example = () => {
  const handleClick = (e) => {
    console.log('target:', e.target.tagName);        // What you clicked
    console.log('currentTarget:', e.currentTarget.tagName); // What has the listener
  };

  return (
    <div onClick={handleClick}>  {/* currentTarget will be DIV */}
      <button>Click me</button>  {/* target will be BUTTON */}
    </div>
  );
};

preventDefault() – Stops Default Browser Behavior:

const FormExample = () => {
  const handleSubmit = (e) => {
    e.preventDefault(); // Prevents form from submitting and page refresh
    console.log('Form submitted via JavaScript instead!');
  };

  const handleLinkClick = (e) => {
    e.preventDefault(); // Prevents link from navigating
    console.log('Link clicked but not navigating!');
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input type="text" />
        <button type="submit">Submit</button>
      </form>

      <a href="https://google.com" onClick={handleLinkClick}>
        This link won't navigate
      </a>
    </div>
  );
};

stopPropagation() – Stops Event Bubbling:

const BubblingExample = () => {
  const handleParentClick = () => {
    console.log('Parent clicked!');
  };

  const handleChildClick = (e) => {
    console.log('Child clicked!');
    e.stopPropagation(); // Prevents parent from also firing
  };

  return (
    <div onClick={handleParentClick} style={{padding: '20px', backgroundColor: 'lightblue'}}>
      Parent
      <button onClick={handleChildClick}>
        Child (click me - parent won't fire)
      </button>
    </div>
  );
};

e.type – Event Type:

const MultiEventExample = () => {
  const handleEvent = (e) => {
    switch(e.type) {
      case 'click':
        console.log('Button was clicked!');
        break;
      case 'mouseenter':
        console.log('Mouse entered button!');
        break;
      case 'mouseleave':
        console.log('Mouse left button!');
        break;
    }
  };

  return (
    <button 
      onClick={handleEvent}
      onMouseEnter={handleEvent}
      onMouseLeave={handleEvent}
    >
      Multi-event button
    </button>
  );
};

4. React Props ๐Ÿ“ฆ Explained

Props (properties) are how you pass data from parent components to child components.

Basic Props Example:

// Parent component
const App = () => {
  const userName = "John";
  const userAge = 25;

  return (
    <div>
      <UserCard name={userName} age={userAge} />
    </div>
  );
};

// Child component receives props
const UserCard = (props) => {
  return (
    <div>
      <h2>Name: {props.name}</h2>
      <p>Age: {props.age}</p>
    </div>
  );
};

Props with Destructuring (More Common):

// Instead of using props.name, props.age
const UserCard = ({ name, age }) => {
  return (
    <div>
      <h2>Name: {name}</h2>
      <p>Age: {age}</p>
    </div>
  );
};

Different Types of Props:

const ComponentExample = () => {
  const user = { name: "Alice", email: "alice@example.com" };
  const numbers = [1, 2, 3, 4, 5];
  const isActive = true;

  return (
    <MyComponent 
      // String prop
      title="Hello World"

      // Number prop
      count={42}

      // Boolean prop
      isVisible={isActive}

      // Object prop
      user={user}

      // Array prop
      items={numbers}

      // Function prop
      onButtonClick={() => console.log('Clicked!')}
    />
  );
};

const MyComponent = ({ title, count, isVisible, user, items, onButtonClick }) => {
  return (
    <div>
      <h1>{title}</h1>
      <p>Count: {count}</p>
      {isVisible && <p>This is visible!</p>}
      <p>User: {user.name} ({user.email})</p>
      <ul>
        {items.map(item => <li key={item}>{item}</li>)}
      </ul>
      <button onClick={onButtonClick}>Click me</button>
    </div>
  );
};

Props in Our Todo App:

// We can break our todo app into smaller components
const TodoApp = () => {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState('');

  const addTodo = () => {
    // ... add todo logic
  };

  const deleteTodo = (id) => {
    // ... delete todo logic
  };

  return (
    <div>
      <AddTodoForm 
        value={inputValue}
        onChange={setInputValue}
        onAdd={addTodo}
      />
      <TodoList 
        todos={todos}
        onDelete={deleteTodo}
      />
    </div>
  );
};

// Child component receives props
const AddTodoForm = ({ value, onChange, onAdd }) => {
  return (
    <div>
      <input 
        type="text"
        value={value}
        onChange={(e) => onChange(e.target.value)}
      />
      <button onClick={onAdd}>Add Todo</button>
    </div>
  );
};

const TodoList = ({ todos, onDelete }) => {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem 
          key={todo.id}
          todo={todo}
          onDelete={onDelete}
        />
      ))}
    </ul>
  );
};

const TodoItem = ({ todo, onDelete }) => {
  return (
    <li>
      {todo.text}
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
};

Props Rules:

  1. Props are read-only – child components cannot modify props
  2. Props flow down – from parent to child, not the other way up
  3. Props can be any data type – strings, numbers, objects, arrays, functions
  4. Props are optional – you can provide default values
// Default props
const Greeting = ({ name = "World", enthusiasm = 1 }) => {
  return <h1>Hello {name}{"!".repeat(enthusiasm)}</h1>;
};

// Usage
<Greeting />                    // "Hello World!"
<Greeting name="Alice" />       // "Hello Alice!"
<Greeting name="Bob" enthusiasm={3} /> // "Hello Bob!!!"

๐ŸŽฏ Summary:

  1. Event Delegation = One listener handles many elements = Better performance
  2. Event parameter naming = Use any name you want (e, event, evt, v, etc.)
  3. Event methods: preventDefault() stops default behavior, stopPropagation() stops bubbling
  4. Event properties: target = what triggered event, currentTarget = what has listener
  5. Props = Data passed from parent to child components

Let’s see in Part 5. Happy React Development! ๐Ÿš€