10) Optional: Redis caching in a Rails API app (why and how)
Even in API-only apps, application-level caching is useful to reduce DB load for expensive queries or aggregated endpoints.
Common patterns
Low-level caching:Rails.cache.fetch
Fragment caching: less relevant for API-only (used for views), but you can cache JSON blobs
Keyed caching with expiration for computed results
Example โ caching an expensive query
class Api::V1::ReportsController < ApplicationController
def sales_summary
key = "sales_summary:#{Time.current.utc.strftime('%Y-%m-%d-%H')}"
data = Rails.cache.fetch(key, expires_in: 5.minutes) do
# expensive computation
compute_sales_summary
end
render json: data
end
end
Why Redis?
Redis is fast, supports expirations, and can be used as Rails.cache store (config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }).
Works well for ephemeral caches that you want to expire automatically.
Invalidation strategies
Time-based (TTL) โ simplest.
Key-based โ when related data changes, evict related keys.
Example: after a product update, call Rails.cache.delete("top_offers").
Versioned keys โ embed a version or timestamp in the key (e.g., products:v3:top) and bump the version on deploy/major change.
Tagging / Key sets โ maintain a set of keys per resource to delete them on change (more manual).
Caveat: Don’t rely solely on Redis caching for user-specific sensitive data. Use private caches when needed.
11) Purging caches and CDN invalidation
When hashed assets are used you rarely need to purge because new filenames mean new URLs. For non-hashed assets or CDN caches you may need purge:
CDN invalidation: Cloudflare / Fastly / CloudFront offer purge by URL or cache key. Use CDN APIs or surrogate-key headers to do group purges.
Surrogate-Control / Surrogate-Key (Fastly): set headers that help map objects to tags for efficient purging.
Nginx cache purge: if you configure proxy_cache, you must implement purge endpoints or TTLs.
Recommended approach:
Prefer hashed filenames for assets so you rarely need invalidation.
For dynamic content, prefer short TTLs or implement programmatic CDN purges as part of deploy/administration flows.
12) Testing and verifying caching behavior (practical commands)
def show
resource = Resource.find(params[:id])
if stale?(etag: resource, last_modified: resource.updated_at)
render json: resource
end
end
Redis caching (Rails.cache)
data = Rails.cache.fetch("top_products_page_#{params[:page]}", expires_in: 5.minutes) do
Product.top.limit(20).to_a
end
render json: data
Conclusion (Part 4)
This part explained where caching belongs in an API-only Rails + Vue application, how Passenger fits into the stack, how to set cache headers for safe API caching, optional Redis caching strategies, and practical testing/operational steps.
In Part 3 we explain how Passenger sits behind Nginx in an API-only Rails app, where caching belongs in the stack, how to safely cache API responses (or avoid caching sensitive ones), and how to verify behavior. This part covers Passenger role, request lifecycle, and API response caching strategy.
1) Passenger’s role in a Rails API app โ what it is and how it plugs into Nginx
What is Passenger? Passenger (Phusion Passenger) is an application server that runs Ruby apps (Rails) and integrates tightly with web servers such as Nginx. It manages application processes, handles spawning, lifecycle, zero-downtime restarts, and serves Rack apps directly without a separate reverse-proxy layer.
Why using Passenger in your stack matters:
Nginx serves static files directly (fast).
If a request cannot be served as a static file, Nginx hands it to Passenger, which invokes your Rails app (API).
Passenger takes care of Ruby processes, workers, memory limits, restarts, etc., reducing operational complexity compared to orchestrating your own Puma cluster + systemd.
Passenger is enabled per-server or per-location. Static files under root are resolved by nginx first โ Passenger only gets requests that do not map to files (or that you explicitly route to Passenger).
Browser requests https://www.mydomain.com/some/path (or /vite/index-ABC.js, or /api/v1/products).
Nginx checks if the request maps to a static file under root /apps/mydomain/current/public.
If file exists โ serve it directly and attach headers (Cache-Control, etc.).
If not โ pass the request to Passenger.
Passenger receives the request and dispatches it to a Rails process.
Rails API processes the request (controllers -> models -> DB) and produces a response JSON or status.
Rails returns the response to Passenger โ Passenger returns it to Nginx โ Nginx returns it to the browser.
Key layers where caching can occur:
Client-side (browser) โ controlled by Cache-Control returned from server.
Reverse-proxy or CDN โ e.g., Cloudflare, Fastly, CloudFront; caching behavior influenced by s-maxage and surrogate headers.
Application caching (Rails/Redis) โ memoization or precomputed JSON payloads to reduce DB cost.
Nginx (edge) caching โ possible for static assets; less common for dynamic Rails responses when using Passenger (but possible with proxying setups).
3) Where API caching should sit (principles)
Because your Rails app is API-only, you should carefully control caching:
Static assets (JS/CSS/fonts/images) = Nginx (1-year for hashed assets).
API responses (JSON) = usually short-lived or uncached unless content is highly cacheable and non-sensitive. If cached:
Prefer caching at CDN layer (s-maxage) or using application-level caching (Rails + Redis).
Use cache validation (ETag, Last-Modified) to enable conditional requests and 304 responses.
Sensitive endpoints (auth, user-specific data) = never cached publicly. Use Cache-Control: no-store, private.
4) Cache-Control and related headers for APIs โ recommended practices
Important response headers and their recommended usage
Cache-Control:
no-store โ do not store response anywhere (safest for sensitive data).
no-cache โ caches may store but must revalidate with origin before use (useful if you want caching but require revalidation).
private โ response intended for a single user; shared caches (CDNs) must not store it.
public โ response may be stored by browsers and CDNs.
max-age=SECONDS โ TTL in seconds.
s-maxage=SECONDS โ TTL for shared caches (CDNs); supersedes max-age for shared caches.
must-revalidate / proxy-revalidate โ force revalidation after expiration.
stale-while-revalidate / stale-if-error โ allow stale responses while revalidation or in case of errors (good for resilience).
ETag:
Strong validator; server generates a value representing resource state. Client includes If-None-Match on subsequent requests. Server returns 304 Not Modified if ETag matches.
Last-Modified and If-Modified-Since:
Based on timestamp; less precise than ETag but simple.
Vary:
Tells caches that responses vary by certain request headers (e.g., Vary: Accept-Encoding or Vary: Authorization).
Example header patterns
Public, CDN cacheable API (e.g., public product listings): Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30 ETag: "abc123" Vary: Accept-Encoding
Browser caches for 60s. CDN caches for 300s. Meanwhile allow stale while revalidate.
5) How to add caching headers in a Rails API controller (practical examples)
Because you run an API-only app, prefer setting headers in controllers selectively for GET endpoints you consider safe to cache.
Basic manual header (safe and explicit):
class Api::V1::ProductsController < ApplicationController
def index
@products = Product.popular.limit(20)
# set short-lived cache for 60 seconds for browsers
response.set_header('Cache-Control', 'public, max-age=60, s-maxage=300, stale-while-revalidate=30')
render json: @products
end
end
Using conditional GET with ETag / Last-Modified:
class Api::V1::ProductsController < ApplicationController
def show
product = Product.find(params[:id])
# This helps return 304 Not Modified if product hasn't changed
if stale?(etag: product, last_modified: product.updated_at)
render json: product
end
end
end
Notes: stale? and fresh_when are provided by ActionController::ConditionalGet. In an API-only app these helper methods are normally available, but confirm by checking your ApplicationController inheritance; if not, you can use response.set_header('ETag', ...) directly.
Setting ETag manually:
etag_value = Digest::SHA1.hexdigest(product.updated_at.to_i.to_s + product.id.to_s)
response.set_header('ETag', "\"#{etag_value}\"")
# then Rails will respond with 304 if If-None-Match matches
6) Important rules for API caching
Only cache GET responses. Never cache responses to POST, PUT, PATCH, DELETE.
Do not cache user-specific or sensitive info in shared caches. Use private or no-store.
Prefer CDN caching (s-maxage) for public endpoints. Use s-maxage to instruct CDNs to keep content longer than browsers.
Use ETags or Last-Modified for validation to reduce bandwidth and get 304 responses.
Consider short TTLs and stale-while-revalidate to reduce origin load while keeping content fresh.
Version your API (e.g., /api/v1/) so you can change caching behavior on new releases without conflicting with old clients.
7) Nginx + Passenger and caching for API endpoints โ what to do (and what to avoid)
Avoid using Nginx proxy cache with Passenger by default. Passenger is not a reverse proxy; it’s an app server. Nginx can use proxy_cache for caching upstream responses, but that pattern is more common when you proxy to a separate Puma/Unicorn backend via proxy_pass. With Passenger, it’s simpler and safer to set cache headers in Rails and let CDNs or clients respect them.
If you want edge caching in Nginx, it is technically possible to enable fastcgi_cache/proxy_cache patterns if you have an upstream; use caution โ caching dynamic JSON responses at the web server is tricky and must be carefully invalidated.
Recommended: set caching headers in Rails (as shown), then let a CDN (Cloudflare/Fastly/CloudFront) apply caching and invalidation; Passenger remains the process manager for Rails.
8) Example: making a public, cacheable endpoint safe and CDN-friendly
class Api::V1::PublicController < ApplicationController
def top_offers
data = Offer.top(10) # expensive query
response.set_header('Cache-Control', 'public, max-age=120, s-maxage=600, stale-while-revalidate=30')
# Optionally set ETag
fresh_when(etag: Digest::SHA1.hexdigest(data.map(&:updated_at).join(',')))
render json: data
end
end
max-age=120 โ browsers cache for 2 minutes
s-maxage=600 โ CDN caches for 10 minutes
stale-while-revalidate=30 โ CDN/browsers may serve stale for 30s while origin revalidates
9) Passenger vs Puma โ quick comparison (for API deployments)
Passenger
Pros:
Tight nginx integration (simpler config).
Auto-manages application processes; zero-downtime restarts are straightforward (passenger-config restart-app).
Good defaults for concurrency and memory management.
Cons:
Less flexible for custom proxy patterns (compared to running Puma behind nginx).
Some advanced caching/proxy setups are easier with a dedicated reverse-proxy architecture.
Puma (common alternative)
Pros:
Lightweight, highly configurable; often used behind nginx as reverse proxy.
Works well in containerized environments (Docker/Kubernetes).
Easy to pair with systemd or process managers and to horizontally scale workers.
Cons:
Requires extra process management & reverse proxying (nginx proxy_pass) configuration.
Slightly more operational overhead vs Passenger.
For an API-only Rails app with static assets served by nginx, Passenger is a great choice when you want fewer moving pieces. Puma + nginx gives more flexibility if you need advanced proxy caching or plan to run in a container orchestration platform.
I’ll continue with Part 4 covering Redis caching (optional), invalidation strategies, testing, debugging, commands, examples of common pitfalls and a final checklist.
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:
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:
These projects rely heavily on content hashing + Nginx headers โ exactly what we’re setting up here.
โ Best Practices Recap
Always fingerprint (hash) assets in production builds.
Cache HTML for 0 seconds, JS/CSS hashed files for 1 year.
Use immutable for hashed assets.
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).
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.
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:
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:
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:
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:
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.
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.
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: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: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
Command
Use Case
Data Safety
Speed
db:migrate
Incremental updates
โ Safe
Medium
db:setup
Initial setup
โ Safe (new DB)
Fast
db:reset
Clean slate
โ Destroys all
Fast
db:migrate:reset
Test migrations
โ Destroys all
Slow
db:schema:load
Fresh schema
โ No data migration
Fast
db:seed
Add sample data
โ Additive
Fast
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.
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.
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)
# 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
# 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.
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.
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
# โ 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
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)
# 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 Type
Sensitivity
Client Access Needed
Recommended Storage
Theme preferences
Low
Yes
Plain cookies
Discount codes
Medium
No
Signed cookies
User settings
Medium
No
Encrypted cookies
Authentication
High
No
Session
Credit card data
High
No
Database + session ID
Shopping cart
Medium
No
Session or encrypted
CSRF tokens
High
Limited
Session (built-in)
Common Pitfalls to Avoid
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
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
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.
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
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).
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.
Welcome to my new series where I combine the power of Ruby with the discipline of Test-Driven Development (TDD) to tackle popular algorithm problems from LeetCode! ๐งโ๐ป๐ Whether you’re a Ruby enthusiast looking to sharpen your problem-solving skills, or a developer curious about how TDD can transform the way you approach coding challenges, you’re in the right place.
๐ฒ Episode 7: Minimum Size Subarray Sum
###########################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose @sum is greater than or equal to target. If there is no such subarray, return 0 instead.
#
# Example 1:
#
# Input: target = 7, nums = [2,3,1,2,4,3]
# Output: 2
# Explanation: The subarray [4,3] has the minimal length under the problem constraint.
# Example 2:
#
# Input: target = 4, nums = [1,4,4]
# Output: 1
# Example 3:
#
# Input: target = 11, nums = [1,1,1,1,1,1,1,1]
# Output: 0
#
#
# Constraints:
#
# 1 <= target <= 109
# 1 <= nums.length <= 105
# 1 <= nums[i] <= 104
#
###########################################################
# โ Fail
# frozen_string_literal: true
#######################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose sum is greater than or equal to target. If there is no such subarray, return 0 instead.
#
#######################################################
require 'minitest/autorun'
require_relative 'subarray_sum_min_size'
class TestSubArraySumMinSize < Minitest::Test
def set_up; end
def test_array_of_length_one
assert_equal 0, SubArray.new([2], 3).min_size
assert_equal 1, SubArray.new([2], 2).min_size
assert_equal 0, SubArray.new([3], 4).min_size
end
end
Source Code:
# frozen_string_literal: true
# disable rubocop GuardClause for better readability in the code
###########################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose @sum is greater than or equal to target. If there is no such subarray, return 0 instead.
# ............
#
###########################################################
class SubArray
def min_size
end
end
โ ruby test_subarray_sum_min_size.rb
Run options: --seed 5914
# Running:
E
Finished in 0.000386s, 2590.6736 runs/s, 0.0000 assertions/s.
1) Error:
TestSubArraySumMinSize#test_array_of_length_one:
ArgumentError: wrong number of arguments (given 2, expected 0)
test_subarray_sum_min_size.rb:16:in 'BasicObject#initialize'
test_subarray_sum_min_size.rb:16:in 'Class#new'
test_subarray_sum_min_size.rb:16:in 'TestSubArraySumMinSize#test_array_of_length_one'
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
โ minimum-size-subarray-sum git:(main) โ
โ Green: Making it pass
# Pass โ
# frozen_string_literal: true
###########################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose sum is greater than or equal to target. If there is no such subarray, return 0 instead.
#
# Example 1:
#........
#
###########################################################
class SubArray
def initialize(nums, target)
@nums = nums
@target = target
end
def min_size
0 if @nums.length == 1 && @nums.first < @target
end
end
# Solution for upto 5 Array Input Length โ
# frozen_string_literal: true
# disable rubocop GuardClause for better readability in the code
# rubocop:disable Style/GuardClause
###########################################################
# ...............
###########################################################
class SubArray
def initialize(nums, target)
@nums = nums
@target = target
@min_length = 0 # default 0 -> solution not found
@left_pos = 0
@right_pos = 0
@sum = nil
end
def min_size
while @right_pos < @nums.length
# first position where left and right positions are at starting point
@sum = if @left_pos.zero? && @right_pos.zero?
@nums[@right_pos]
else
# add elements inside the window
@nums[@left_pos..@right_pos].sum
end
if solution_found?
update_min_length
return 1 if @min_length == 1 # best scenario found, stop here
else
@right_pos += 1 # increase window size by 1
end
end
@min_length
end
private
def update_min_length
new_length = @right_pos - @left_pos + 1
if min_length_empty? || min_or_equal_length?(new_length)
@min_length = new_length
@left_pos += 1
end
end
def solution_found?
@sum >= @target
end
def min_length_empty?
@min_length.zero?
end
# if new length of subarray found is less than already found min length
# or new length found is equal to previous min length (should decrease window size
# by increasing left pos to find the less length subarray)
def min_or_equal_length?(new_length)
new_length <= @min_length
end
end
# Solution 1 โ
# frozen_string_literal: true
# disable rubocop GuardClause for better readability in the code
# rubocop:disable Style/GuardClause
###########################################################
# #209
# .............
###########################################################
class SubArray
def initialize(nums, target)
@nums = nums
@target = target
@min_length = 0 # default 0 -> solution not found
@left_pos = 0
@right_pos = 0
@sum = nil
end
def min_size
while @right_pos < @nums.length
@sum = calculate_sum
if solution_found?
update_min_length
return 1 if @min_length == 1 # best scenario found, stop here
else
@right_pos += 1 # increase window size by 1
end
end
@min_length
end
private
def calculate_sum
# first position where left and right positions are at starting point
return @nums[@right_pos] if @left_pos.zero? && @right_pos.zero?
# add elements inside the window
@nums[@left_pos..@right_pos].sum
end
def update_min_length
new_length = @right_pos - @left_pos + 1
if min_length_empty? || min_or_equal_length?(new_length)
@min_length = new_length
@left_pos += 1
end
end
def solution_found?
@sum >= @target
end
def min_length_empty?
@min_length.zero?
end
# if new length of subarray found is less than already found min length
# or new length found is equal to previous min length (should decrease window size
# by increasing left pos to find the less length subarray)
def min_or_equal_length?(new_length)
new_length <= @min_length
end
end
# Solution 2 โ
# frozen_string_literal: true
# disable rubocop GuardClause for better readability in the code
###########################################################
# #209
# .............
###########################################################
class SubArray
def initialize(nums, target)
@nums = nums
@target = target
@min_length = 0 # default 0 -> solution not found
@left_pos = 0
@right_pos = 0
@sum = nil
end
def min_size
while @right_pos < @nums.length
@sum = calculate_sum
if solution_found?
update_min_length
return 1 if @min_length == 1 # best scenario found, stop here
else
@right_pos += 1 # increase window size by 1
end
end
@min_length
end
private
def calculate_sum
# first position where left and right positions are at starting point
return @nums[@right_pos] if @left_pos.zero? && @right_pos.zero?
# add elements inside the window
@nums[@left_pos..@right_pos].sum
end
def update_min_length
new_length = @right_pos - @left_pos + 1
@min_length = new_length if min_length_empty? || min_length_greater?(new_length)
@left_pos += 1
end
def solution_found?
@sum >= @target
end
def min_length_empty?
@min_length.zero?
end
# if new length of subarray found is less than already found min length
# or new length found is equal to previous min length (should decrease window size
# by increasing left pos to find the less length subarray)
def min_length_greater?(new_length)
@min_length > new_length
end
end
๐งฎ Algorithm Complexity Analysis
Time Complexity: O(nยฒ)
Our current algorithm has quadratic time complexity due to the calculate_sum method:
def calculate_sum(nums, left_pos, right_pos)
# This line causes O(n) complexity in each iteration
nums[left_pos..right_pos].sum
end
Why O(nยฒ)?
Outer loop: while right_pos < nums.length โ O(n)
Solution: We should change this logic of repeated addition of numbers that are already added before. We can add the next Number (Right position) and substract the Left Number that is out of the window.
Space Complexity: O(1)
Only uses a constant number of variables regardless of input size
No additional data structures that grow with input
๐ Optimized Version (O(n) Time):
Here’s how to make it linear time complexity:
Let’s Try to Optimize our solution with the Solution given above:
# frozen_string_literal: true
# disable rubocop GuardClause for better readability in the code
###########################################################
# ..................
###########################################################
class SubArray
def initialize(nums, target)
@nums = nums
@target = target
@min_length = 0 # default 0 -> solution not found
@left_pos = 0
@right_pos = 0
@sum = 0
end
def min_size
while @right_pos < @nums.length
# Add the new element at right_pos to the current sum
@sum += @nums[@right_pos]
update_min_length if solution_found?
@right_pos += 1 # always move right pointer
end
@min_length
end
private
def update_min_length
new_length = @right_pos - @left_pos + 1
@min_length = new_length if min_length_empty? || min_length_greater?(new_length)
# Shrink the window from the left as much as possible while maintaining sum >= target
while @left_pos < @right_pos && (@sum - @nums[@left_pos]) >= @target
@sum -= @nums[@left_pos]
@left_pos += 1
new_length = @right_pos - @left_pos + 1
@min_length = new_length if min_length_greater?(new_length)
end
end
def solution_found?
@sum >= @target
end
def min_length_empty?
@min_length.zero?
end
# if new length of subarray found is less than already found min length
# or new length found is equal to previous min length (should decrease window size
# by increasing left pos to find the less length subarray)
def min_length_greater?(new_length)
@min_length > new_length
end
end
๐ Complexity Comparison:
Version
Time Complexity
Space Complexity
Why
Your Current
O(nยฒ)
O(1)
Recalculates sum each time
Optimized
O(n)
O(1)
Maintains running sum
Key Optimization:
Instead of recalculating the sum each time:
# Your approach (O(n) each time)
nums[left_pos..right_pos].sum
# Optimized approach (O(1) each time)
current_sum += num # Add new element
current_sum -= nums[left] # Remove old element
Our algorithm works correctly but can be optimized from O(nยฒ) to O(n) time complexity!
LeetCode Submission (simplified version of 0(n)):
# @param {Integer} target
# @param {Integer[]} nums
# @return {Integer}
def min_sub_array_len(target, nums)
return 0 if nums.empty?
min_length = Float::INFINITY
left = 0
sum = 0
nums.each_with_index do |num, right|
sum += num
# Shrink window from left as much as possible while maintaining sum >= target
while sum >= target && left <= right
min_length = [min_length, right - left + 1].min
sum -= nums[left]
left += 1
end
end
min_length == Float::INFINITY ? 0 : min_length
end