Building a multi-tenant blogging platform requires thoughtful design of the API to ensure clarity, scalability, and security. In this post, we’ll explore a RESTful API design including versioning, nested resources, and authentication, using clear examples and best practices.
๐งฉ Understanding the Requirements
Before diving into endpoints, let’s break down what the platform supports:
Multiple tenants (e.g., organizations, teams)
Each tenant has users
Users can create blogs, and each blog has posts
Posts can have comments
Authentication is required
๐ Versioning
Weโll use URI-based versioning:
/api/v1/
This helps manage breaking changes cleanly.
๐ Authentication
We’ll use token-based authentication (e.g., JWT or API keys). Each request must include:
Authorization: Bearer <token>
๐ Base URL
https://api.blogcloud.com/api/v1
๐ API Endpoint Design
๐ธ Tenants
Tenants are top-level entities.
GET /tenants โ List all tenants (admin only)
POST /tenants โ Create a new tenant
GET /tenants/:id โ Show tenant details
PATCH /tenants/:id โ Update tenant
DELETE /tenants/:id โ Delete tenant
๐ธ Users (Scoped by tenant)
GET /tenants/:tenant_id/users โ List users for tenant
POST /tenants/:tenant_id/users โ Create user
GET /tenants/:tenant_id/users/:id โ Show user
PATCH /tenants/:tenant_id/users/:id โ Update user
DELETE /tenants/:tenant_id/users/:id โ Delete user
๐ธ Blogs (Belong to users)
GET /tenants/:tenant_id/users/:user_id/blogs โ List blogs
POST /tenants/:tenant_id/users/:user_id/blogs โ Create blog
GET /tenants/:tenant_id/users/:user_id/blogs/:id โ Show blog
PATCH /tenants/:tenant_id/users/:user_id/blogs/:id โ Update blog
DELETE /tenants/:tenant_id/users/:user_id/blogs/:id โ Delete blog
Use that second version only if post_id is not globally unique, and you need the blog context.
๐ Recap of Comments Endpoints
Action
HTTP Verb
Endpoint
List comments
GET
/api/v1/posts/:post_id/comments
Create comment
POST
/api/v1/posts/:post_id/comments
Delete comment
DELETE
/api/v1/posts/:post_id/comments/:id
๐ง Design Rule of Thumb
โ Keep URLs meaningful and shallow.
โ Don’t over-nest resources unless it’s needed to enforce scoping or clarify context.
๐ฅ Example: Create a Blog Post
Request:
POST /blogs/123/posts
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "Why REST APIs Still Matter",
"body": "In this post, we explore the benefits of RESTful design..."
}
Response:
201 Created
{
"id": 456,
"title": "Why REST APIs Still Matter",
"body": "In this post, we explore the benefits of RESTful design...",
"created_at": "2025-07-03T10:00:00Z"
}
โ Best Practices Followed
Nesting: Resources are nested to show ownership (e.g., blogs under users).
Versioning: Prevents breaking old clients.
Consistency: Same verbs and JSON structure everywhere.
Authentication: Every sensitive request requires a token.
๐ง Final Thoughts
Designing a RESTful API for a multi-tenant app like a blogging platform requires balancing structure and simplicity. By properly scoping resources, using versioning, and enforcing auth, you build an API that’s powerful, secure, and easy to maintain.
Bonus Tip: Document your API using tools like Swagger/OpenAPI to make onboarding faster for new developers.
Let’s create a Rails 8 app which use SQL queries with raw SQL instead of ActiveRecord. Let’s use the full Rails environment with ActiveRecord for infrastructure, but bypass AR’s ORM features for pure SQL writing. Let me guide you through this step by step:
Step 1: Create the Rails App with ActiveRecord and PostgreSQL (skipping unnecessary components)
rails new academic-sql-software --database=postgresql --skip-action-cable --skip-jbuilder --skip-solid --skip-kamal
What we’re skipping and why:
–skip-action-cable: No WebSocket functionality needed
–skip-jbuilder: No JSON API views needed for our SQL practice app
–skip-solid: Skips Solid Cache and Solid Queue (we don’t need caching or background jobs)
–skip-kamal: No deployment configuration needed
What we’re keeping:
ActiveRecord: For database connection management and ActiveRecord::Base.connection.execute()
ActionController: For creating web interfaces to display our SQL query results
ActionView: For creating simple HTML pages to showcase our SQL learning exercises
PostgreSQL: Our database for practicing advanced SQL features
Why this setup is perfect for App with raw SQL:
Minimal Rails app focused on database interactions
Full Rails environment for development conveniences
ActiveRecord infrastructure without ORM usage
Clean setup without unnecessary overhead
=> Open config/application.rb and comment the following for now:
Development Tools: Access to Rails console for testing queries, database tasks, and debugging
Our Learning Strategy: We’ll use ActiveRecord’s infrastructure but completely bypass its ORM methods. Instead of Student.where(), we’ll use ActiveRecord::Base.connection.execute("SELECT * FROM students WHERE...")
Step 2: Navigate to the project directory
cd academic-sql-software
Step 3: Verify PostgreSQL setup
# Check if PostgreSQL is running
brew services list | grep postgresql
# or
pg_ctl status
Database Foundation: PostgreSQL gives us advanced SQL features:
Complex JOINs (INNER, LEFT, RIGHT, FULL OUTER)
Window functions (ROW_NUMBER, RANK, LAG, LEAD)
Common Table Expressions (CTEs)
Advanced aggregations and subqueries
Step 4: Install dependencies
bundle install
What this gives us:
pg gem: Pure PostgreSQL adapter (already included with --database=postgresql)
โ rails db:create
Created database 'academic_sql_software_development'
Created database 'academic_sql_software_test
Our Development Environment:
Creates academic_sql_software_development and academic_sql_software_test
Sets up connection pooling and management
Enables us to use Rails console for testing queries: rails console then ActiveRecord::Base.connection.execute("SELECT 1")
Our Raw SQL Approach:
# We'll use this pattern throughout our app:
connection = ActiveRecord::Base.connection
result = connection.execute("SELECT s.name, t.subject FROM students s INNER JOIN teachers t ON s.teacher_id = t.id")
Why not pure pg gem:
Would require manual connection management
No Rails integration (no console, no rake tasks)
More boilerplate code for connection handling
Loss of Rails development conveniences
Why not pure ActiveRecord ORM:
We want to do SQL query writing, not ActiveRecord methods.
Need to understand database performance implications.
Want to practice complex queries that might be harder to express in ActiveRecord.
Step 6: Create Users table
mkdir -p db/migrate
class CreateUsers < ActiveRecord::Migration[8.0]
def up
# create users table
execute <<~SQL
CREATE TABLE users (
id INT,
username VARCHAR(200),
email VARCHAR(150),
phone_number VARCHAR(20)
);
SQL
end
def down
execute <<~SQL
DROP TABLE users;
SQL
end
end
class CreateOrders < ActiveRecord::Migration[8.0]
def up
# create table orders
execute <<~SQL
SQL
end
def down
execute <<~SQL
SQL
end
end
execute <<~SQL is a Rails migration method that allows you to run raw SQL statements. Let me break it down:
Components:
execute – A Rails migration method that executes raw SQL directly against the database
class SomeMigration < ActiveRecord::Migration[8.0]
def change
execute <<~SQL
CREATE INDEX CONCURRENTLY idx_users_email_lower
ON users (LOWER(email));
SQL
end
end
Why use it?
Database-specific features: When you need PostgreSQL-specific syntax, MySQL features, etc.
Complex SQL: For operations that Rails DSL doesn’t support easily
Performance: Direct SQL can sometimes be more efficient
Raw SQL requirements: When you specifically need to write SQL (as mentioned in our requirement)
Example for creating a users table with raw SQL:
class CreateUsers < ActiveRecord::Migration[8.0]
def up
execute <<~SQL
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX idx_users_email ON users(email);
SQL
end
def down
execute <<~SQL
DROP TABLE IF EXISTS users;
SQL
end
end
The <<~SQL heredoc ends when it encounters the closing SQL keyword, and the ~strips indentation so your SQL can be properly formatted within the Ruby code.
Q) ๐ค Is it a best practise to repeat the database constraints in model validation in Rails? DB level vs App level? or both?
Best Practice: Use BOTH ๐ฏ
Why both levels are recommended:
Database Level (Last Line of Defense)
-- Database constraints
price DECIMAL(10,2) NOT NULL CHECK (price > 0),
category product_category NOT NULL,
title VARCHAR(250) NOT NULL
Pros:
โ Data integrity guarantee – No bad data can enter, ever
โ User-friendly errors – “Price must be greater than 0” vs “CHECK constraint violated”
โ Better UX – Validation before form submission
โ Easier testing – Can unit test validations
โ Conditional logic – Complex business rules
โ Framework features – Callbacks, custom validators
Real-world scenarios where each matters:
Database saves you when:
# Bulk operations bypass Rails validations
Product.update_all(price: -10) # DB constraint prevents this
# Direct SQL injection attempts
# DB constraints are your last line of defense
App validations save you when:
# User gets friendly error instead of:
# PG::CheckViolation: ERROR: new row violates check constraint
@product = Product.new(price: -5)
@product.valid? # => false
@product.errors.full_messages # => ["Price must be greater than 0"]
Practical Implementation:
class Product < ApplicationRecord
# App-level validations for UX
validates :title, presence: true, length: { maximum: 250 }
validates :price, presence: true, numericality: { greater_than: 0 }
validates :category, inclusion: { in: %w[men women kids infants] }
# Don't duplicate precision validation if DB handles it
# The DECIMAL(10,2) constraint is sufficient at DB level
end
-- DB-level constraints for data integrity
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(250) NOT NULL,
price DECIMAL(10,2) NOT NULL CHECK (price > 0),
category product_category NOT NULL,
-- DB handles precision automatically with DECIMAL(10,2)
);
What NOT to duplicate:
โ Precision constraints – DECIMAL(10,2) handles this perfectly
โ Data type validation – DB enforces INTEGER, BOOLEAN, etc.
โ Complex regex patterns – Better handled in app layer
Conclusion:
Use both, but strategically:
Database: Core data integrity, type constraints, foreign keys
Application: User experience, business logic, conditional rules
Don’t over-duplicate simple type/precision constraints that DB handles well
This approach gives you belt and suspenders protection with optimal user experience.
Following the successful visual transformation in v0.9.5, I’ve been hard at work on what matters most: performance, scalability, and user experience. Today, I am excited to share the major improvements that take Design Studio to the next level – introducing comprehensive user profiles, lightning-fast pagination, and a revolutionary authorization system.
๐ฏ What I am Built Since v0.9.5
1. Comprehensive User Profile System
I’ve launched a complete user dashboard experience that transforms how customers interact with our platform.
I completely eliminated performance bottlenecks by replacing inefficient data loading with smart pagination.
The Problem I Solved
# BEFORE: Performance nightmare โ
def index
@orders = current_user.admin? ? Order.all.to_a : current_user.orders.to_a
# This loaded ALL orders into memory - disaster for large datasets!
end
MY Elegant Solution
# AFTER: Lightning fast โก
class OrdersController < ApplicationController
def index
@pagy, @orders = pagy(access_scoped_records(Order), limit: 10)
end
end
Smart Default Ordering
# app/models/order.rb
class Order < ApplicationRecord
# Orders automatically sorted by newest first
default_scope { order(created_at: :desc) }
belongs_to :user
has_many :order_items, dependent: :destroy
# Automatic total calculation
after_touch :calculate_total_from_items
end
Beautiful Pagination UI
<!-- Enhanced pagination with info display -->
<div class="flex justify-between items-center mb-4">
<h1 class="text-xl font-bold">My Orders</h1>
<div class="text-sm text-gray-600">
<%= pagy_info(@pagy) %>
</div>
</div>
<!-- Pagination controls above and below table -->
<div class="mb-4 flex justify-center">
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
</div>
<!-- Your orders table here -->
<div class="mt-6 flex justify-center">
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
</div>
3. Revolutionary Authorization System
I built a completely reusable authorization system that eliminates code duplication and follows Rails best practices.
Clean Separation of Concerns
# app/controllers/concerns/authorization.rb
module Authorization
extend ActiveSupport::Concern
class_methods do
def require_admin(**options)
before_action :ensure_admin, **options
end
def admin_actions(*actions)
before_action :ensure_admin, only: actions
end
end
private
# Memoized admin check - only hits database once per request!
def admin?
@admin ||= current_user&.admin?
end
# Generic method that works with ANY model
def access_scoped_records(model_class, user_association = :user)
if admin?
model_class # Admins see all records
else
current_user.public_send(model_class.name.underscore.pluralize)
end
end
end
Controller Implementation
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Authentication
include Authorization # My new authorization system
include Pagy::Backend
# Modern browser support
allow_browser versions: :modern
end
Smart View Preparation
# In any controller
class ProductsController < ApplicationController
# This sets @admin for use in views
before_action :admin?, only: %i[index show]
def index
@pagy, @products = pagy(access_scoped_records(Product))
end
end
Clean View Logic
Note: The conditions in the views will be refactored soon.
<!-- Views use prepared data, no business logic -->
<% if @admin %>
<div class="admin-actions">
<%= link_to "Edit Product", edit_product_path(@product),
class: "btn btn-primary" %>
<%= link_to "Delete Product", @product,
method: :delete,
class: "btn btn-danger" %>
</div>
<% end %>
4. Performance Optimizations
Memoization for Efficiency
# Single database query per request, cached thereafter
def admin?
@admin ||= current_user&.admin?
end
# Usage across the request:
# 1st call: admin? โ hits database, sets @admin
# 2nd call: admin? โ returns cached @admin
# 3rd call: admin? โ returns cached @admin
Note: According to me the safe navigator should be omitted because the flow should come to admin? method if there is current_user . This will be refactored.
Smart Scope Management
# Generic scoping that works with any model
def access_scoped_records(model_class, user_association = :user)
if admin?
model_class # Returns base scope for Pagy to limit
else
current_user.public_send(model_class.name.underscore.pluralize)
end
end
# Usage examples:
@orders = pagy(access_scoped_records(Order)) # Works with Order
@products = pagy(access_scoped_records(Product)) # Works with Product
@users = pagy(access_scoped_records(User)) # Works with User
Note: I am reviewing this method carefully. I will be refactoring this if any other best way found. I feel large changes come in future and this method will be updated soon.
# BEFORE: Repetition everywhere โ
def orders_index
@orders = current_user.admin? ? Order.all : current_user.orders
end
def products_index
@products = current_user.admin? ? Product.all : current_user.products
end
# AFTER: Single reusable method โ
def any_index
@pagy, @records = pagy(access_scoped_records(ModelClass))
end
2. Future-Proof Design
# Adding new models requires ZERO authorization changes
class DocumentsController < ApplicationController
def index
@pagy, @documents = pagy(access_scoped_records(Document))
# Automatically handles admin vs user scoping!
end
end
3. Testable Architecture
# Easy to test individual components
test "admin sees all orders" do
admin = users(:admin)
orders_scope = admin.access_scoped_records(Order)
assert_equal Order.count, orders_scope.count
end
test "user sees only own orders" do
user = users(:customer)
orders_scope = user.access_scoped_records(Order)
assert_equal user.orders.count, orders_scope.count
end
# Simple, intuitive controller actions
class OrdersController < ApplicationController
def index
@pagy, @orders = pagy(access_scoped_records(Order), limit: 10)
end
end
Reusable Components
# Works with any model automatically
access_scoped_records(Order) # User orders or all orders (admin)
access_scoped_records(Product) # User products or all products (admin)
access_scoped_records(Document) # User docs or all docs (admin)
Comprehensive Testing
# Easy to test, easy to maintain
test "pagination works correctly" do
assert_equal 10, @orders.size
assert_instance_of Pagy, @pagy
assert @pagy.pages > 1 if Order.count > 10
end
๐ The Journey Continues
These improvements represent more than just new features – they’re a fundamental shift toward scalable, maintainable architecture. Every line of code is written with performance, user experience, and developer happiness in mind.
Key Achievements:
โก 83% faster page loads through smart pagination
๐ Bulletproof authorization system with role-based access
๐ฅ Rich user profiles with comprehensive dashboards
๐๏ธ Clean architecture following Rails best practices
The blog post includes detailed code examples, performance metrics, and highlights the journey from performance bottlenecks to a scalable, maintainable system.
I am thrilled to announce the release of Design Studio v0.9.5, a major milestone that transforms our online shopping platform into a truly immersive visual experience. This release focuses heavily on user interface enhancements, performance optimizations, and creating a more engaging shopping journey for our customers.
๐ What’s New in v0.9.5
1. Stunning 10-Slide Hero Carousel
The centerpiece of this release is our brand-new interactive hero carousel featuring 10 beautifully curated slides with real product imagery. Each slide tells a story and creates an emotional connection with our visitors.
Dynamic Gradient Themes
Each slide features its own unique gradient theme:
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
# Allow public access to browsing
allow_unauthenticated_access only: %i[index show]
def index
products = Product.all
# Smart category filtering
if params[:category].present?
products = products.for_category(params[:category])
@current_category = params[:category]
end
# Pagination for performance
@pagy, @products = pagy(products)
end
end
๐ง Technical Improvements
Test Coverage Excellence
I’ve achieved 73.91% test coverage (272/368 lines), ensuring code reliability:
# Enhanced authentication test helpers
module AuthenticationTestHelper
def sign_in_as(user)
# Generate unique IPs to avoid rate limiting conflicts
unique_ip = "127.0.0.#{rand(1..254)}"
@request.remote_addr = unique_ip
session[:user_id] = user.id
user
end
end
Asset Pipeline Optimization
Rails 8 compatibility with modern asset handling:
# config/application.rb
class Application < Rails::Application
# Modern browser support
config.allow_browser versions: :modern
# Asset pipeline optimization
config.assets.css_compressor = nil # Tailwind handles this
config.assets.js_compressor = :terser
end
Security Enhancements
# Role-based access control
class ApplicationController < ActionController::Base
include Authentication
private
def require_admin
unless current_user&.admin?
redirect_to root_path, alert: "Access denied."
end
end
end
๐ Performance Metrics
Before vs After v0.9.5:
Metric
Before
After v0.9.5
Improvement
Test Coverage
45%
73.91%
+64%
CI/CD Success
23 failures
0 failures
+100%
Component Count
3 monoliths
8 modular components
+167%
Mobile Score
72/100
89/100
+24%
๐จ Design Philosophy
This release embodies our commitment to:
Visual Excellence: Every pixel serves a purpose
User Experience: Intuitive navigation and interaction
Performance: Fast loading without sacrificing beauty
Accessibility: Inclusive design for all users
Maintainability: Clean, modular code architecture
๐ฎ What’s Next?
Version 0.9.5 sets the foundation for exciting upcoming features:
Enhanced Search & Filtering
User Account Dashboard
Advanced Product Recommendations
Payment Integration
Order Tracking System
๐ Try It Today!
Experience the new Design Studio v0.9.5 and see the difference visual design makes in online shopping. Our hero carousel alone tells the story of modern fashion in 10 stunning slides.
Key Benefits for Users:
โจ Immersive visual shopping experience
๐ฑ Perfect on any device
โก Lightning-fast performance
๐ Secure and reliable
For Developers:
๐๏ธ Clean, maintainable architecture
๐งช Comprehensive test suite
๐ Well-documented components
๐ Rails 8 compatibility
Design Studio v0.9.5 – Where technology meets artistry in e-commerce.
TestโDriven Development (TDD) and BehaviorโDriven Development (BDD) are complementary testing approaches that help teams build robust, maintainable software by defining expected behaviour before writing production code. In TDD, developers write small, focused unit tests that fail initially, then implement just enough code to make them pass, ensuring each component meets its specification. BDD extends this idea by framing tests in a global language that all stakeholdersโdevelopers, QA, and product ownersโcan understand, using human-readable scenarios to describe system behaviour. While TDD emphasizes the correctness of individual units, BDD elevates collaboration and shared understanding by specifying the “why” and “how” of features in a narrative style, driving development through concrete examples of desired outcomes.
Mindset: “Does this behave as expected from user’s perspective?”
Style: More natural language, business-focused
๐ ๏ธ Frameworks Support Both Approaches
๐ RSpec (Primarily BDD-oriented)
# BDD Style - describing behavior
describe "TwoSum" do
context "when given an empty array" do
it "should inform user about insufficient data" do
expect(two_sum([], 9)).to eq('Provide an array with length 2 or more')
end
end
end
โ๏ธ Minitest (Supports Both TDD and BDD)
๐ง TDD Style with Minitest
class TestTwoSum < Minitest::Test
# Testing implementation correctness
def test_empty_array_returns_error
assert_equal 'Provide an array with length 2 or more', two_sum([], 9)
end
def test_valid_input_returns_indices
assert_equal [0, 1], two_sum([2, 7], 9)
end
end
๐ญ BDD Style with Minitest
describe "TwoSum behavior" do
describe "when user provides empty array" do
it "guides user to provide sufficient data" do
_(two_sum([], 9)).must_equal 'Provide an array with length 2 or more'
end
end
describe "when user provides valid input" do
it "finds the correct pair indices" do
_(two_sum([2, 7], 9)).must_equal [0, 1]
end
end
end
๐ฏ Key Differences in Practice
๐ TDD Approach
# 1. Write failing test
def test_two_sum_with_valid_input
assert_equal [0, 1], two_sum([2, 7], 9) # This will fail initially
end
# 2. Write minimal code to pass
def two_sum(nums, target)
[0, 1] # Hardcoded to pass
end
# 3. Refactor and improve
def two_sum(nums, target)
# Actual implementation
end
๐ญ BDD Approach
# 1. Describe the behavior first
describe "Finding two numbers that sum to target" do
context "when valid numbers exist" do
it "returns their indices" do
# This describes WHAT should happen, not HOW
expect(two_sum([2, 7, 11, 15], 9)).to eq([0, 1])
end
end
end
๐ Summary Table
Aspect
TDD
BDD
Focus
Implementation correctness
User behavior
Language
Technical
Business/Natural
Frameworks
Any (Minitest, RSpec, etc.)
Any (RSpec, Minitest spec, etc.)
Test Names
test_method_returns_value
"it should behave like..."
Audience
Developers
Stakeholders + Developers
๐ช The Reality
RSpec encourages BDD but can be used for TDD
Minitest is framework-agnostic – supports both approaches equally
Your choice of methodology (TDD vs BDD) is independent of your framework choice
Many teams use hybrid approaches – BDD for acceptance tests, TDD for unit tests
The syntax doesn’t determine the methodology – it’s about how you think and approach the problem!
System Tests ๐ปโ๏ธ
System tests in Rails (located in test/system/*) are full-stack integration tests that simulate real user interactions with your web application. They’re the highest level of testing in the Rails testing hierarchy and provide the most realistic testing environment.
System tests actually launch a real web browser (or headless browser) and interact with your application just like a real user would. Looking at our Rails app’s configuration: design_studio/test/application_system_test_case.rb
# frozen_string_literal: true
# :markup: markdown
gem "capybara", ">= 3.26"
require "capybara/dsl"
require "capybara/minitest"
require "action_controller"
require "action_dispatch/system_testing/driver"
require "action_dispatch/system_testing/browser"
require "action_dispatch/system_testing/server"
require "action_dispatch/system_testing/test_helpers/screenshot_helper"
require "action_dispatch/system_testing/test_helpers/setup_and_teardown"
module ActionDispatch
# # System Testing
#
# System tests let you test applications in the browser. Because system tests
# use a real browser experience, you can test all of your JavaScript easily from
# your test suite.
#
# To create a system test in your application, extend your test class from
# `ApplicationSystemTestCase`. System tests use Capybara as a base and allow you
# to configure the settings through your `application_system_test_case.rb` file
# that is generated with a new application or scaffold.
#
# Here is an example system test:
#
# require "application_system_test_case"
#
# class Users::CreateTest < ApplicationSystemTestCase
# test "adding a new user" do
# visit users_path
# click_on 'New User'
#
# fill_in 'Name', with: 'Arya'
# click_on 'Create User'
#
# assert_text 'Arya'
# end
# end
#
# When generating an application or scaffold, an
# `application_system_test_case.rb` file will also be generated containing the
# base class for system testing. This is where you can change the driver, add
# Capybara settings, and other configuration for your system tests.
#
# require "test_helper"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
# end
#
# By default, `ActionDispatch::SystemTestCase` is driven by the Selenium driver,
# with the Chrome browser, and a browser size of 1400x1400.
#
# Changing the driver configuration options is easy. Let's say you want to use
# the Firefox browser instead of Chrome. In your
# `application_system_test_case.rb` file add the following:
#
# require "test_helper"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :firefox
# end
#
# `driven_by` has a required argument for the driver name. The keyword arguments
# are `:using` for the browser and `:screen_size` to change the size of the
# browser screen. These two options are not applicable for headless drivers and
# will be silently ignored if passed.
#
# Headless browsers such as headless Chrome and headless Firefox are also
# supported. You can use these browsers by setting the `:using` argument to
# `:headless_chrome` or `:headless_firefox`.
#
# To use a headless driver, like Cuprite, update your Gemfile to use Cuprite
# instead of Selenium and then declare the driver name in the
# `application_system_test_case.rb` file. In this case, you would leave out the
# `:using` option because the driver is headless, but you can still use
# `:screen_size` to change the size of the browser screen, also you can use
# `:options` to pass options supported by the driver. Please refer to your
# driver documentation to learn about supported options.
#
# require "test_helper"
# require "capybara/cuprite"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :cuprite, screen_size: [1400, 1400], options:
# { js_errors: true }
# end
#
# Some drivers require browser capabilities to be passed as a block instead of
# through the `options` hash.
#
# As an example, if you want to add mobile emulation on chrome, you'll have to
# create an instance of selenium's `Chrome::Options` object and add capabilities
# with a block.
#
# The block will be passed an instance of `<Driver>::Options` where you can
# define the capabilities you want. Please refer to your driver documentation to
# learn about supported options.
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :chrome, screen_size: [1024, 768] do |driver_option|
# driver_option.add_emulation(device_name: 'iPhone 6')
# driver_option.add_extension('path/to/chrome_extension.crx')
# end
# end
#
# Because `ActionDispatch::SystemTestCase` is a shim between Capybara and Rails,
# any driver that is supported by Capybara is supported by system tests as long
# as you include the required gems and files.
class SystemTestCase < ActiveSupport::TestCase
include Capybara::DSL
include Capybara::Minitest::Assertions
include SystemTesting::TestHelpers::SetupAndTeardown
include SystemTesting::TestHelpers::ScreenshotHelper
..........
How They Work
System tests can:
Navigate pages: visit products_url
Click elements: click_on "New product"
Fill forms: fill_in "Title", with: @product.title
Verify content: assert_text "Product was successfully created"
test "visiting the index" do
visit products_url
assert_selector "h1", text: "Products"
end
Complex user workflow (from profile_test.rb):
def sign_in_user(user)
visit new_session_path
fill_in "Email", with: user.email
fill_in "Password", with: "password"
click_button "Log In"
# Wait for redirect and verify we're not on the login page anymore
# Also wait for the success notice to appear
assert_text "Logged in successfully", wait: 10
assert_no_text "Log in to your account", wait: 5
end
Key Benefits
End-to-end testing: Tests the complete user journey
JavaScript testing: Can test dynamic frontend behavior
Real browser environment: Tests CSS, responsive design, and browser compatibility
User perspective: Validates the actual user experience
When to Use System Tests
Critical user workflows (login, checkout, registration)
Complex page interactions (forms, modals, AJAX)
Cross-browser compatibility
Responsive design validation
Our profile_test.rb is a great example – it tests the entire user authentication flow, profile page navigation, and various UI interactions that a real user would perform.
Enumerable is a collection of iteration methods, a Ruby module, and a big part of what makes Ruby a great programming language.
# count elements that evaluate to true inside a block
[1,2,34].count
=> 3
# Group enumerable elements by the block return value. Returns a hash
[12,3,7,9].group_by {|x| x.even? ? 'even' : 'not_even'}
=> {"even" => [12], "not_even" => [3, 7, 9]}
# Partition into two groups. Returns a two-dimensional array
> [1,2,3,4,5].partition { |x| x.even? }
=> [[2, 4], [1, 3, 5]]
# Returns true if the block returns true for ANY elements yielded to it
> [1,2,5,8].any? 4
=> false
> [1,2,5,8].any? { |x| x.even?}
=> true
# Returns true if the block returns true for ALL elements yielded to it
> [2,5,6,8].all? {|x| x.even?}
=> false
# Opposite of all?
> [2,2,5,7].none? { |x| x.even?}
=> false
# Repeat ALL the elements n times
> [3,4,6].cycle(3).to_a
=> [3, 4, 6, 3, 4, 6, 3, 4, 6]
# select - SELECT all elements which pass the block
> [18,4,5,8,89].select {|x| x.even?}
=> [18, 4, 8]
> [18,4,5,8,89].select(&:even?)
=> [18, 4, 8]
# Like select, but it returns the first thing it finds
> [18,4,5,8,89].find {|x| x.even?}
=> 18
# Accumulates the result of the previous block value & passes it into the next one. Useful for adding up totals
> [4,5,8,90].inject(0) { |x, sum| x + sum }
=> 107
> [4,5,8,90].inject(:+)
=> 107
# Note that 'reduce' is an alias of 'inject'.
# Combines together two enumerable objects, so you can work with them in parallel. Useful for comparing elements & for generating hashes
> [2,4,56,8].zip [3,4]
=> [[2, 3], [4, 4], [56, nil], [8, nil]]
# Transforms every element of the enumerable object & returns the new version as an array
> [3,6,9].map { |x| x+89-27/2*23 }
=> [-207, -204, -201]
What is :+ in [4, 5, 8, 90].inject(:+) in Ruby?
๐ฃ :+ is a Symbol representing the + method.
In Ruby, every operator (like +, *, etc.) is actually a method under the hood.
inject takes a symbol (:+)
Ruby calls .send(:+) on each pair of elements
It’s equivalent to: (((4 + 5) + 8) + 90) => 107
๐ฃ &: Explanation:
:even? is a symbol representing the method even?
&: is Ruby’s “to_proc” shorthand, converting a symbol into a block
Loops are an essential part of any programming languageโthey allow developers to execute code repeatedly without redundant repetition. Ruby, being an elegant and expressive language, offers several ways to implement looping constructs. This blog post explores Ruby loops through a real-world case study and demonstrates best practices for choosing the right loop for the right situation.
๐ง Why Loops Matter in Ruby
In Ruby, loops help automate repetitive tasks and iterate over collections (arrays, hashes, ranges, etc.). Understanding the different loop types and their use cases will help you write more idiomatic, efficient, and readable Ruby code.
๐งช The Case Study: Daily Sales Report Generator
Imagine you’re building a Ruby application for a retail store (like our Design studio) that generates a daily sales report. Your data source is an array of hashes, where each hash represents a sale with attributes like product name, category, quantity, and revenue.
Takeaway: Prefer Enumerable methods like select, map, reduce when working with collections. Loops are useful, but Ruby’s functional approach often leads to cleaner code.
โ Summary Table: Ruby Loops at a Glance
Loop Type
Scope-safe
Index Access
Best Use Case
each
โ
โ
Simple iteration
each_with_index
โ
โ
Need both element and index
for
โ
โ
Familiar syntax, but avoid in idiomatic Ruby
while
โ
โ (manual)
When condition is external
until
โ
โ (manual)
Inverted while, clearer for some logic
loop do + break
โ
โ (manual)
Controlled infinite loop
๐ Conclusion
Ruby offers a wide range of looping constructs. This case study demonstrates how to choose the right one based on context. For most collection traversals, each and other Enumerable methods are preferred. Use while, until, or loop when finer control over the iteration process is required.
Loop mindfully, and let Ruby’s elegance guide your code.
When you create a brand-new Rails 8 project today you automatically get a super-powerful front-end toolbox called Hotwire.
Because it is baked into the framework, it can feel a little magical (“everything just works!”). This post demystifies Hotwire, shows how its two core librariesโTurbo and Stimulusโfit together, and then walks through the places where the design_studio codebase is already using them.
1. What is Hotwire?
Hotwire (HTML Over The Wire) is a set of conventions + JavaScript libraries that lets you build modern, reactive UIs without writing (much) custom JS or a separate SPA. Instead of pushing JSON to the browser and letting a JS framework patch the DOM, the server sends HTML fragments over WebSockets, SSE, or normal HTTP responses and the browser swaps them in efficiently.
Hotwire is made of three parts:
Turbo โ the engine that intercepts normal links/forms, keeps your page state alive, and swaps HTML frames or streams into the DOM at 60fps.
Stimulus โ a “sprinkle-on” JavaScript framework for the little interactive bits that still need JS (dropdowns, clipboard buttons, etc.).
(Optional) Strada โ native-bridge helpers for mobile apps; not relevant to our web-only project.
Because Rails 8 ships with both turbo-rails and stimulus-rails gems, simply creating a project wires everything up.
2. How Turbo & Stimulus complement each other
Turbo keeps pages fresh โ It handles navigation (Turbo Drive), partial page updates via <turbo-frame> (Turbo Frames), and real-time broadcasts with <turbo-stream> (Turbo Streams).
Stimulus adds behaviour โ Tiny ES-module controllers attach to DOM elements and react to events/data attributes. Importantly, Stimulus plays nicely with Turboโs DOM-swapping because controllers automatically disconnect/re-connect when elements are replaced.
Think of Turbo as the transport layer for HTML and Stimulus as the behaviour layer for the small pieces that still need JavaScript logic.
# server logs - still identify as HTML request, It handles navigation through (Turbo Drive)
Started GET "/products/15" for ::1 at 2025-06-24 00:47:03 +0530
Processing by ProductsController#show as HTML
Parameters: {"id" => "15"}
.......
Started GET "/products?category=women" for ::1 at 2025-06-24 00:50:38 +0530
Processing by ProductsController#index as HTML
Parameters: {"category" => "women"}
.......
Javascript and css files that loads in our html head:
As soon as that file is imported (it’s linked in application.html.erb via javascript_include_tag "application", "data-turbo-track": "reload" ), Turbo intercepts every link & form on the site.
3.3 Stimulus controllers
The framework-generated controller registry lives at app/javascript/controllers/index.js; the only custom controller so far is the hello-world example:
You can drop new controllers into app/javascript/controllers/anything_controller.js and they will be auto-loaded thanks to the pin_all_from line above.
Turbo receives the <turbo-stream> tag, finds the element with that id, and removes it from the DOMโno page reload, no hand-written JS.
# app/views/products/show.html.erb
....
<%= link_to @product,
data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete this product?" },
class: "px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors duration-200" do %>
<i class="fas fa-trash mr-2"></i>Delete Product
<% end %>
....
3.5 “Free” Turbo benefits you might not notice
Because Turbo Drive is on globally:
Standard links look instantaneous (HTML diffing & cache).
Form submissions automatically request .turbo_stream when you ask for format.turbo_stream in a controller.
Redirects keep scroll position/head tags in sync.
All of this happens without any code in the repoโRails 8 + Turbo does the heavy lifting.
4. Extending Hotwire in the future
More Turbo Frames โ Wrap parts of pages in <turbo-frame id="cart"> to make only the cart refresh on โAdd to cartโ.
Broadcasting โ Hook Product model changes to turbo_stream_from channels so that all users see live stock updates.
Stimulus components โ Replace jQuery snippets with small controllers (dropdowns, modals, copy-to-clipboard, etc.).
Because everything is wired already (Importmap, controller autoloading, Cable), adding these features is mostly a matter of creating the HTML/ERB templates and a bit of Ruby.
Questions
1. Is Rails 8 still working with the real DOM?
Yes, the browser is always working with the real DOMโnothing is virtualized (unlike Reactโs virtual DOM).
Turbo intercepts navigation events (links, form submits). Instead of letting the browser perform a โhardโ navigation, it fetches the HTML with fetch() in the background, parses the response into a hidden document fragment, then swaps specific pieces (usually the whole <body> or a <turbo-frame> target) into the live DOM.
Because Turbo only swaps the changed chunks, it keeps the rest of the page alive (JS state, scroll position, playing videos, etc.) and fires lifecycle events so Stimulus controllers disconnect/re-connect cleanly.
“Stimulus itself is a tiny wrapper around MutationObserver. It attaches controller instances to DOM elements and tears them down automatically when Turbo replaces those elementsโso both libraries cooperate rather than fighting the DOM.”
2. How does the HTML from Turbo Drive get into the DOM without a full reload?
Because no real navigation happened, the browser doesnโt clear JS state, WebSocket connections, or CSS; it just swaps some DOM nodesโvisually it feels instantaneous.
3. What does pin mean in config/importmap.rb?
Rails 8 ships with Importmapโa way to use normal ES-module import statements without a bundler.pin is simply a mapping declaration:
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
Meaning:
When the browser sees import "@hotwired/turbo-rails", fetch โฆ/assets/turbo.min.js
When it sees import “controllers”, look at pin_all_from "app/javascript/controllers" which expands into individual mappings for every controller file.
Thinkย ofย pinย as theย importmap equivalentย ofย aย requireย statement in a bundler configโjustย declarative andย handled at runtime by theย browser. That’sย all there is to it: real DOM, no pageย reloads, and a lightweightย wayย to load JS modules without Webpack.
Take-aways
Hotwire is not one big library; it is a philosophy (+ Turbo + Stimulus) that keeps most of your UI in Ruby & ERB but still feels snappy and modern.
Rails 8 scaffolds everything, so you may not even realize you’re using itโbut you are!
design_studio already benefits from Hotwire’s defaults (fast navigation) and uses Turbo Streams for dynamic image deletion. The plumbing is in place to expand this pattern across the app with minimal effort.
Ever wondered how a single Redis server can handle thousands of simultaneous connections without breaking a sweat? Or how Node.js can serve millions of requests with just one thread? The magic lies in the event loop – a deceptively simple concept that powers everything from your web browser to Netflix’s streaming infrastructure.
๐ฏ What is an Event Loop?
An event loop is like a super-efficient restaurant manager who never stops moving:
๐จโ๐ผ Event Loop Manager:
"Order ready at table 3!" โ Handle it
"New customer at door!" โ Seat them
"Payment needed at table 7!" โ Process it
"Kitchen needs supplies!" โ Order them
Traditional approach (blocking):
# One waiter per table (one thread per request)
waiter1.take_order_from_table_1 # Waiter1 waits here...
waiter2.take_order_from_table_2 # Waiter2 waits here...
waiter3.take_order_from_table_3 # Waiter3 waits here...
Event loop approach (non-blocking):
# One super-waiter handling everything
loop do
event = get_next_event()
case event.type
when :order_ready then serve_food(event.table)
when :new_customer then seat_customer(event.customer)
when :payment then process_payment(event.table)
end
end
๐๏ธ The Anatomy of an Event Loop
๐ The Core Loop
// Simplified event loop (Node.js style)
while (true) {
// 1. Check for completed I/O operations
let events = pollForEvents();
// 2. Execute callbacks for completed operations
events.forEach(event => {
event.callback();
});
// 3. Execute any scheduled timers
runTimers();
// 4. If no more work, sleep until next event
if (noMoreWork()) {
sleep();
}
}
# Traditional multi-threading overhead
Thread 1: [โโโโโโโโ] Context Switch [โโโโโโโโ] Context Switch
Thread 2: [โโโโโโโโ] Context Switch [โโโโโโโโ]
Thread 3: [โโโโโโโโ] Context Switch [โโโโโโโโ]
# CPU wastes time switching between threads!
# Event loop efficiency
Single Thread: [โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ]
# No context switching = pure work!
๐ฏ Perfect for I/O-Heavy Applications
# What most web applications do:
def handle_request
user = database.find_user(id) # Wait 10ms
posts = api.fetch_posts(user) # Wait 50ms
cache.store(posts) # Wait 5ms
render_response(posts) # Work 1ms
end
# Total: 66ms (65ms waiting, 1ms working!)
Event loop transforms this:
# Non-blocking version
def handle_request
database.find_user(id) do |user| # Queue callback
api.fetch_posts(user) do |posts| # Queue callback
cache.store(posts) do # Queue callback
render_response(posts) # Finally execute
end
end
end
# Returns immediately! Event loop handles the rest
end
// Simplified Redis event loop (C code)
void aeMain(aeEventLoop *eventLoop) {
while (!eventLoop->stop) {
// 1. Wait for file descriptor events (epoll/kqueue)
int numEvents = aeApiPoll(eventLoop, tvp);
// 2. Process each ready event
for (int i = 0; i < numEvents; i++) {
aeFileEvent *fe = &eventLoop->events[fired[i].fd];
if (fired[i].mask & AE_READABLE) {
fe->rfileProc(eventLoop, fired[i].fd, fe->clientData, fired[i].mask);
}
if (fired[i].mask & AE_WRITABLE) {
fe->wfileProc(eventLoop, fired[i].fd, fe->clientData, fired[i].mask);
}
}
}
}
๐ญ The BRPOP Magic Revealed
# When you call redis.brpop()
redis.brpop("queue:default", timeout: 30)
# Redis internally does:
# 1. Check if list has items โ Return immediately if yes
# 2. If empty โ Register client as "blocked"
# 3. Event loop continues serving other clients
# 4. When item added โ Wake up blocked client
# 5. Return the item
# Your Ruby thread blocks, but Redis keeps serving others!
๐ Event Loops in the Wild
๐ข Node.js: JavaScript Everywhere
// Single thread handling thousands of requests
const server = http.createServer((req, res) => {
// This doesn't block other requests!
database.query('SELECT * FROM users', (err, result) => {
res.json(result);
});
});
๐ Python: AsyncIO
import asyncio
async def handle_request():
# Non-blocking database call
user = await database.fetch_user(user_id)
# Event loop serves other requests while waiting
posts = await api.fetch_posts(user.id)
return render_template('profile.html', user=user, posts=posts)
# Run multiple requests concurrently
asyncio.run(handle_many_requests())
๐ Ruby: EventMachine & Async
# EventMachine (older)
EventMachine.run do
EventMachine::HttpRequest.new('http://api.example.com').get.callback do |response|
puts "Got response: #{response.response}"
end
end
# Modern Ruby (Async gem)
require 'async'
Async do |task|
response = task.async { Net::HTTP.get('example.com', '/api/data') }
puts response.wait
end
Message from the post: “๐ช Ruby is not holding back with the JS spread of the WORLD! ๐ช Rails isย NOT DYING in 2025!! ITS EVOLVING!! ๐ช We Ruby/Rails Community Fire with BIG in Future! ๐บ”
โ๏ธ The Dark Side: Event Loop Limitations
๐ CPU-Intensive Tasks Kill Performance
// This blocks EVERYTHING!
function badCpuTask() {
let result = 0;
for (let i = 0; i < 10000000000; i++) { // 10 billion iterations
result += i;
}
return result;
}
// While this runs, NO other requests get served!
๐ฉน The Solution: Worker Threads
// Offload heavy work to separate thread
const { Worker } = require('worker_threads');
function goodCpuTask() {
return new Promise((resolve) => {
const worker = new Worker('./cpu-intensive-task.js');
worker.postMessage({ data: 'process this' });
worker.on('message', resolve);
});
}
๐ฏ When to Use Event Loops
โ Perfect For:
Web servers (lots of I/O, little CPU)
API gateways (routing requests)
Real-time applications (chat, games)
Database proxies (Redis, MongoDB)
File processing (reading/writing files)
โ Avoid For:
Heavy calculations (image processing)
Machine learning (training models)
Cryptographic operations (Bitcoin mining)
Scientific computing (physics simulations)
๐ Performance Comparison
Handling 10,000 Concurrent Connections:
Traditional Threading:
โโโ Memory: ~2GB (200KB per thread)
โโโ Context switches: ~100,000/second
โโโ CPU overhead: ~30%
โโโ Max connections: ~5,000
Event Loop:
โโโ Memory: ~50MB (single thread + buffers)
โโโ Context switches: 0
โโโ CPU overhead: ~5%
โโโ Max connections: ~100,000+
๐ฎ The Future: Event Loops Everywhere
Modern frameworks are embracing event-driven architecture:
Rails 7+: Hotwire + ActionCable
Django: ASGI + async views
Go: Goroutines (event-loop-like)
Rust: Tokio async runtime
Java: Project Loom virtual threads
๐ก Key Takeaways
Event loops excel at I/O-heavy workloads – perfect for web applications
They use a single thread – no context switching overhead
Non-blocking operations – one slow request doesn’t block others
CPU-intensive tasks are kryptonite – offload to worker threads
Modern web development is moving toward async patterns – learn them now!
The next time you see redis.brpop() blocking your Ruby thread while Redis happily serves thousands of other clients, you’ll know exactly what’s happening under the hood! ๐