Ruby on Rails - Rapid Web Application Development
Ruby on Rails
backendRuby on Rails - Rapid Web Application Development
Ruby on Rails, commonly known simply as Rails, is a web framework written in Ruby that has been setting standards in rapid application development for over two decades. Created by David Heinemeier Hansson in 2004 and extracted from the Basecamp project, Rails revolutionized the approach to building web applications. GitHub, Shopify, Airbnb, Basecamp, Twitch, and Hulu are just some of the companies that built their products on this framework.
In this article, we will explore the key features of Ruby on Rails in detail, covering its architecture, essential tools, and the techniques that make it one of the most productive frameworks for building web applications.
The Convention over Configuration Philosophy#
The fundamental principle of Rails is Convention over Configuration (CoC). This means the framework provides sensible defaults for virtually every aspect of an application. Instead of writing dozens of configuration files, developers follow established naming conventions and project structures.
How Does It Work in Practice?#
# Model - file app/models/article.rb
# Rails automatically maps it to the 'articles' table in the database
class Article < ApplicationRecord
belongs_to :author
has_many :comments, dependent: :destroy
has_many :taggings
has_many :tags, through: :taggings
validates :title, presence: true, length: { minimum: 5 }
validates :body, presence: true
end
In the example above, you do not need to define the table name, primary key, or columns. Rails assumes that:
- The
Articlemodel corresponds to thearticlestable - The primary key is
id - The foreign key in the
commentstable isarticle_id - Timestamps
created_atandupdated_atare handled automatically
The second key principle is Don't Repeat Yourself (DRY). Rails actively promotes the elimination of code duplication through abstractions, helpers, and shared components.
MVC Architecture in Rails#
Rails implements the Model-View-Controller (MVC) architectural pattern, which separates application logic into three layers:
Model - The Data Layer#
Models represent application data and business logic. They are linked to database tables through Active Record:
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
has_many :articles, foreign_key: :author_id
has_many :comments
has_one :profile
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :username, presence: true,
uniqueness: true,
length: { in: 3..30 }
scope :active, -> { where(active: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
before_save :normalize_email
private
def normalize_email
self.email = email.downcase.strip
end
end
Controller - The Logic Layer#
Controllers handle HTTP requests, communicate with models, and render responses:
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_article, only: [:show, :edit, :update, :destroy]
before_action :authorize_article!, only: [:edit, :update, :destroy]
def index
@articles = Article.includes(:author, :tags)
.published
.order(created_at: :desc)
.page(params[:page])
.per(15)
end
def show
@comments = @article.comments.includes(:user).order(created_at: :asc)
@comment = Comment.new
end
def create
@article = current_user.articles.build(article_params)
if @article.save
redirect_to @article, notice: "Article has been published."
else
render :new, status: :unprocessable_entity
end
end
def update
if @article.update(article_params)
redirect_to @article, notice: "Article has been updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@article.destroy
redirect_to articles_path, notice: "Article has been deleted."
end
private
def set_article
@article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:title, :body, :published, tag_ids: [])
end
def authorize_article!
redirect_to root_path unless @article.author == current_user
end
end
View - The Presentation Layer#
Views generate HTML responses using ERB (Embedded Ruby) templates:
<%# app/views/articles/show.html.erb %>
<article class="article-detail">
<header>
<h1><%= @article.title %></h1>
<div class="meta">
<span>Author: <%= @article.author.username %></span>
<time datetime="<%= @article.created_at.iso8601 %>">
<%= l(@article.created_at, format: :long) %>
</time>
</div>
<div class="tags">
<% @article.tags.each do |tag| %>
<%= link_to tag.name, tag_path(tag), class: "tag" %>
<% end %>
</div>
</header>
<div class="content">
<%= @article.body.html_safe %>
</div>
<section class="comments">
<h2>Comments (<%= @comments.count %>)</h2>
<%= render @comments %>
<%= render "comments/form", comment: @comment, article: @article %>
</section>
</article>
Active Record ORM#
Active Record is the heart of Rails - the ORM (Object-Relational Mapping) layer that elegantly connects Ruby objects to database tables.
Database Migrations#
Migrations allow you to version your database schema and share changes across the team:
# db/migrate/20250128_create_articles.rb
class CreateArticles < ActiveRecord::Migration[7.1]
def change
create_table :articles do |t|
t.string :title, null: false
t.text :body, null: false
t.boolean :published, default: false
t.references :author, null: false, foreign_key: { to_table: :users }
t.datetime :published_at
t.integer :views_count, default: 0
t.timestamps
end
add_index :articles, :published
add_index :articles, :published_at
add_index :articles, [:author_id, :created_at]
end
end
Running migrations is straightforward:
# Run all pending migrations
rails db:migrate
# Rollback the last migration
rails db:rollback
# Check migration status
rails db:migrate:status
# Reset the database
rails db:reset
Associations#
Active Record offers a rich set of associations for modeling relationships:
class Author < ApplicationRecord
has_many :articles, dependent: :destroy
has_many :comments, through: :articles
has_one :profile, dependent: :destroy
has_and_belongs_to_many :roles
end
class Article < ApplicationRecord
belongs_to :author
has_many :comments, dependent: :destroy
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
has_one_attached :cover_image # Active Storage
has_rich_text :body # Action Text
end
class Comment < ApplicationRecord
belongs_to :article, counter_cache: true
belongs_to :user
has_many :replies, class_name: "Comment", foreign_key: :parent_id
belongs_to :parent, class_name: "Comment", optional: true
end
Validations#
Rails provides an extensive validation system:
class User < ApplicationRecord
validates :email, presence: true,
uniqueness: true,
format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
validates :password, length: { minimum: 8 },
if: :password_required?
validates :age, numericality: { greater_than_or_equal_to: 18 },
allow_nil: true
validate :custom_validation
private
def custom_validation
if username&.include?("admin") && !admin?
errors.add(:username, "cannot contain the word 'admin'")
end
end
end
Database Queries#
Active Record provides an elegant interface for building queries:
# Searching
Article.where(published: true)
.where("created_at > ?", 1.week.ago)
.order(created_at: :desc)
.limit(10)
# Eager loading - solving the N+1 problem
Article.includes(:author, :tags, :comments)
.where(published: true)
# Aggregation
Article.group(:author_id)
.count
Article.where(published: true)
.average(:views_count)
# Advanced queries
Article.joins(:author)
.where(authors: { active: true })
.select("articles.*, authors.username as author_name")
Scaffolding and Generators#
One of the most powerful features of Rails is the generator system, which automates the creation of boilerplate code:
# Scaffold - generates model, controller, views, migration, tests
rails generate scaffold Product name:string description:text \
price:decimal{10,2} stock:integer category:references
# Model generator
rails generate model Order user:references total:decimal \
status:string shipped_at:datetime
# Controller generator
rails generate controller Api::V1::Products index show create update destroy
# Migration generator
rails generate migration AddSlugToArticles slug:string:uniq
# Job generator
rails generate job SendWelcomeEmail
# Mailer generator
rails generate mailer UserMailer welcome reset_password
After running rails generate scaffold Product, Rails creates a complete set of files:
- Model with validations
- Database migration
- Controller with full CRUD operations
- Views (index, show, new, edit, _form)
- Unit and system tests
- Routing
Action Cable - WebSockets in Rails#
Action Cable integrates WebSockets directly into the framework, enabling real-time communication:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
@room = ChatRoom.find(params[:room_id])
stream_for @room
end
def receive(data)
message = @room.messages.create!(
user: current_user,
body: data["body"]
)
ChatChannel.broadcast_to(@room, {
id: message.id,
body: message.body,
user: message.user.username,
created_at: message.created_at.strftime("%H:%M")
})
end
def unsubscribed
# Cleanup on disconnect
end
end
Client-side implementation with JavaScript:
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
const chatChannel = consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: roomId },
{
connected() {
console.log("Connected to chat")
},
received(data) {
const messagesContainer = document.getElementById("messages")
messagesContainer.insertAdjacentHTML("beforeend", `
<div class="message">
<strong>${data.user}:</strong>
<span>${data.body}</span>
<time>${data.created_at}</time>
</div>
`)
},
sendMessage(body) {
this.perform("receive", { body: body })
}
}
)
Active Job - Background Processing#
Active Job provides a unified interface for handling background tasks, regardless of the chosen backend (Sidekiq, Resque, Delayed Job):
# app/jobs/send_newsletter_job.rb
class SendNewsletterJob < ApplicationJob
queue_as :mailers
retry_on Net::SMTPError, wait: 5.minutes, attempts: 3
discard_on ActiveJob::DeserializationError
def perform(newsletter)
newsletter.subscribers.find_each do |subscriber|
NewsletterMailer.weekly_digest(subscriber, newsletter).deliver_now
end
newsletter.update!(sent_at: Time.current)
end
end
# Enqueue a job
SendNewsletterJob.perform_later(newsletter)
# Schedule for later
SendNewsletterJob.set(wait: 1.hour).perform_later(newsletter)
# Schedule for a specific time
SendNewsletterJob.set(wait_until: Date.tomorrow.noon).perform_later(newsletter)
Configuration with Sidekiq#
# config/application.rb
config.active_job.queue_adapter = :sidekiq
# config/sidekiq.yml
:concurrency: 10
:queues:
- [critical, 3]
- [default, 2]
- [mailers, 1]
- [low, 1]
Turbo and Hotwire - Modern Frontend in Rails 7+#
Rails 7 introduced Hotwire as the default approach to building interactive interfaces without writing extensive JavaScript. Hotwire consists of three components:
Turbo Drive#
Automatically converts full-page navigation into asynchronous requests:
<%# Turbo Drive works automatically - navigation is faster
without any additional code %>
<nav>
<%= link_to "Home", root_path %>
<%= link_to "Articles", articles_path %>
<%= link_to "Profile", profile_path %>
</nav>
Turbo Frames#
Allow partial page updates:
<%# app/views/articles/index.html.erb %>
<%= turbo_frame_tag "articles_list" do %>
<% @articles.each do |article| %>
<%= render article %>
<% end %>
<%# Pagination loads within the frame without page reload %>
<%= link_to "Next page",
articles_path(page: @page + 1),
data: { turbo_frame: "articles_list" } %>
<% end %>
Turbo Streams#
Enable real-time updates directly from the server:
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
@comment = @article.comments.build(comment_params)
@comment.user = current_user
if @comment.save
respond_to do |format|
format.turbo_stream
format.html { redirect_to @article }
end
else
render :new, status: :unprocessable_entity
end
end
end
<%# app/views/comments/create.turbo_stream.erb %>
<%= turbo_stream.append "comments" do %>
<%= render @comment %>
<% end %>
<%= turbo_stream.update "comment_count", @article.comments.count %>
<%= turbo_stream.replace "comment_form" do %>
<%= render "comments/form", comment: Comment.new, article: @article %>
<% end %>
Stimulus#
A lightweight JavaScript framework for adding behaviors to HTML:
// app/javascript/controllers/search_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "results"]
static values = { url: String }
connect() {
this.timeout = null
}
search() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.fetchResults()
}, 300)
}
async fetchResults() {
const query = this.inputTarget.value
if (query.length < 2) return
const response = await fetch(`${this.urlValue}?q=${query}`)
this.resultsTarget.innerHTML = await response.text()
}
}
Testing with RSpec and Minitest#
Rails has built-in support for testing. Minitest is the default testing framework, but many developers prefer RSpec.
Minitest#
# test/models/article_test.rb
class ArticleTest < ActiveSupport::TestCase
setup do
@article = articles(:valid_article)
end
test "should not save article without title" do
@article.title = nil
assert_not @article.valid?
assert_includes @article.errors[:title], "can't be blank"
end
test "should not save article with short title" do
@article.title = "Hi"
assert_not @article.valid?
end
test "should belong to author" do
assert_respond_to @article, :author
assert_instance_of User, @article.author
end
end
RSpec#
# spec/models/article_spec.rb
RSpec.describe Article, type: :model do
describe "validations" do
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_length_of(:title).is_at_least(5) }
it { is_expected.to validate_presence_of(:body) }
end
describe "associations" do
it { is_expected.to belong_to(:author).class_name("User") }
it { is_expected.to have_many(:comments).dependent(:destroy) }
it { is_expected.to have_many(:tags).through(:taggings) }
end
describe "#publish!" do
let(:article) { create(:article, published: false) }
it "marks article as published" do
article.publish!
expect(article.published).to be true
expect(article.published_at).to be_present
end
end
describe ".recent" do
it "returns articles from last week" do
recent = create(:article, created_at: 2.days.ago)
old = create(:article, created_at: 2.weeks.ago)
expect(Article.recent).to include(recent)
expect(Article.recent).not_to include(old)
end
end
end
# spec/requests/articles_spec.rb
RSpec.describe "Articles API", type: :request do
describe "GET /api/v1/articles" do
before { create_list(:article, 5, published: true) }
it "returns published articles" do
get "/api/v1/articles"
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body).size).to eq(5)
end
end
describe "POST /api/v1/articles" do
let(:user) { create(:user) }
let(:valid_params) { { article: { title: "New Article", body: "Content" } } }
it "creates article for authenticated user" do
post "/api/v1/articles",
params: valid_params,
headers: auth_headers(user)
expect(response).to have_http_status(:created)
expect(Article.count).to eq(1)
end
end
end
Security in Rails#
Rails includes built-in protection mechanisms against the most common attacks:
CSRF Protection#
# Automatically enabled in ApplicationController
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
The CSRF token is automatically included in forms:
<%= form_with(model: @article) do |form| %>
<%# CSRF token is added automatically %>
<%= form.text_field :title %>
<%= form.submit "Save" %>
<% end %>
XSS Protection#
Rails automatically escapes output in views:
<%# Safe - automatic escaping %>
<p><%= @article.title %></p>
<%# Unsafe - use only with trusted data %>
<p><%= sanitize @article.body %></p>
<%# Content Security Policy %>
<%# config/initializers/content_security_policy.rb %>
SQL Injection Protection#
# Safe - parameterized queries
User.where("email = ?", params[:email])
User.where(email: params[:email])
# Unsafe - NEVER do this!
# User.where("email = '#{params[:email]}'")
Strong Parameters#
# Control which parameters can be mass-assigned
def article_params
params.require(:article).permit(:title, :body, :published, tag_ids: [])
end
Rails API Mode#
Rails is excellent for building APIs. API mode removes unnecessary middleware and provides a leaner configuration:
# Create a new application in API mode
rails new my_api --api
# app/controllers/api/v1/articles_controller.rb
module Api
module V1
class ArticlesController < ApplicationController
before_action :authenticate_api_user!
def index
articles = Article.includes(:author, :tags)
.published
.page(params[:page])
.per(25)
render json: articles,
each_serializer: ArticleSerializer,
meta: pagination_meta(articles)
end
def show
article = Article.find(params[:id])
render json: article, serializer: ArticleDetailSerializer
end
def create
article = current_user.articles.build(article_params)
if article.save
render json: article,
serializer: ArticleSerializer,
status: :created
else
render json: { errors: article.errors.full_messages },
status: :unprocessable_entity
end
end
private
def pagination_meta(collection)
{
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count
}
end
end
end
end
Serialization with ActiveModel::Serializer#
# app/serializers/article_serializer.rb
class ArticleSerializer < ActiveModel::Serializer
attributes :id, :title, :excerpt, :published_at, :views_count
belongs_to :author, serializer: AuthorSerializer
has_many :tags, serializer: TagSerializer
def excerpt
object.body.truncate(200)
end
end
The Gems Ecosystem#
One of the greatest advantages of Rails is its rich library ecosystem (gems). Here are the most popular ones:
| Gem | Purpose | |-----|---------| | Devise | User authentication | | Pundit | Authorization and access policies | | Sidekiq | Background job processing | | Kaminari | Pagination | | Ransack | Advanced search | | Active Admin | Admin panel | | CarrierWave | File uploads | | RuboCop | Code linter and formatter | | FactoryBot | Test data factories | | Capistrano | Deployment automation | | Bullet | N+1 query detection | | Pagy | High-performance pagination |
Example Gemfile Configuration#
# Gemfile
source "https://rubygems.org"
gem "rails", "~> 7.1"
gem "pg", "~> 1.5"
gem "puma", ">= 6.0"
gem "redis", ">= 5.0"
# Authentication and authorization
gem "devise", "~> 4.9"
gem "pundit", "~> 2.3"
# API
gem "jbuilder"
gem "rack-cors"
# Background processing
gem "sidekiq", "~> 7.0"
# Frontend
gem "turbo-rails"
gem "stimulus-rails"
gem "tailwindcss-rails"
group :development, :test do
gem "rspec-rails", "~> 6.0"
gem "factory_bot_rails"
gem "faker"
gem "rubocop-rails", require: false
gem "bullet"
end
group :development do
gem "web-console"
gem "letter_opener"
end
Who Uses Ruby on Rails?#
Rails is a production-grade technology powering some of the largest platforms on the internet:
- GitHub - the largest code hosting platform (over 100 million users)
- Shopify - e-commerce platform serving millions of stores
- Basecamp - project management tool (creators of Rails)
- Airbnb - global accommodation rental marketplace
- Twitch - streaming platform
- Hulu - streaming service
- Zendesk - customer support platform
- Kickstarter - crowdfunding platform
- Dribbble - designer community
These companies prove that Rails handles scaling and serving millions of users exceptionally well.
Performance - Practical Tips#
Rails is sometimes criticized for performance, but with proper practices it achieves excellent results:
Query Optimization#
# Eager loading - eliminating N+1
Article.includes(:author, :tags, comments: :user).published
# Counter cache - avoiding COUNT queries
# Migration: add_column :articles, :comments_count, :integer, default: 0
belongs_to :article, counter_cache: true
# Database indexing
add_index :articles, [:published, :created_at]
add_index :articles, :slug, unique: true
Caching#
# Fragment caching
<% cache @article do %>
<article>
<h2><%= @article.title %></h2>
<p><%= @article.body %></p>
</article>
<% end %>
# Russian Doll caching
<% cache @article do %>
<%= render @article.comments %>
<% end %>
# Low-level caching
Rails.cache.fetch("stats/articles_count", expires_in: 1.hour) do
Article.published.count
end
Production Deployment#
# config/environments/production.rb
Rails.application.configure do
config.cache_classes = true
config.eager_load = true
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }
config.active_job.queue_adapter = :sidekiq
config.force_ssl = true
end
Summary#
Ruby on Rails remains one of the most productive frameworks for building web applications. Its key advantages include:
- Development speed - Convention over Configuration and generators accelerate development
- Maturity - over 20 years of development and proven solutions
- Completeness - everything you need in a single framework
- Ecosystem - thousands of gems for every need
- Security - built-in protection against CSRF, XSS, and SQL Injection
- Testing - TDD culture built into the framework
- Hotwire - modern frontend without SPA complexity
- Scalability - GitHub and Shopify prove that Rails scales excellently
Need Help?#
At MDS Software Solutions Group, we help businesses build modern web applications using Ruby on Rails and other backend technologies. We offer:
- Building web applications from scratch with Ruby on Rails
- Migrating existing systems to modern architecture
- Building APIs and integrations with external systems
- Performance optimization of existing Rails applications
- Architectural consulting and code review
Contact us to discuss your project and discover how Rails can accelerate the development of your product!
Team of programming experts specializing in modern web technologies.