Przejdź do treści
Backend

Ruby on Rails - Rapid Web Application Development

Published on:
·6 min read·Author: MDS Software Solutions Group

Ruby on Rails

backend

Ruby 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 Article model corresponds to the articles table
  • The primary key is id
  • The foreign key in the comments table is article_id
  • Timestamps created_at and updated_at are 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!

Author
MDS Software Solutions Group

Team of programming experts specializing in modern web technologies.

Ruby on Rails - Rapid Web Application Development | MDS Software Solutions Group | MDS Software Solutions Group