Rails-Specific Features - A Comprehensive Guide to MVC Implementation

Rails-Specific Features for MVC Pattern

A comprehensive guide to Rails features that make MVC implementation elegant and secure.

Table of Contents

  1. Controllers
  2. Models
  3. Views
  4. Routing
  5. Additional Features

Controllers

Action Controller Parameters

One of Rails’ most important security features is Strong Parameters (also known as Action Controller Parameters). This feature helps prevent mass assignment vulnerabilities by requiring you to explicitly specify which parameters are allowed in your controller actions.

💡 Security Note: Strong Parameters were introduced to prevent mass assignment vulnerabilities, where malicious users could potentially set any attribute by manipulating the request parameters.

Basic Usage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# In a Rails controller
class PersonController < ApplicationController
  def create
    # Only permit :name and :age parameters from the :person hash
    person_params = params.require(:person).permit(:name, :age)

    # Safe to use for mass assignment
    @person = Person.create(person_params)
  end
end

Key Methods

  1. require: Ensures a parameter is present and returns the parameter
1
2
3
# params = { person: { name: "John", age: 30 } }
params.require(:person)  # Returns the person hash
params.require(:admin)   # Raises ActionController::ParameterMissing
  1. permit: Specifies which parameters are allowed
1
2
3
4
5
# Allowing specific parameters
params.permit(:name, :age)

# Allowing nested parameters
params.permit(user: [:name, :age, address: [:street, :city]])
  1. expect: Combines require and permit in one step
1
2
3
4
5
# Instead of:
params.require(:person).permit(:name, :age)

# You can write:
params.expect(person: [:name, :age])

Example with Nested Parameters

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Complex parameter structure
params = ActionController::Parameters.new({
  person: {
    name: "John",
    age: 30,
    address: {
      street: "123 Main St",
      city: "Boston"
    },
    hobbies: ["reading", "coding"]
  }
})

# Permitting nested parameters
permitted = params.permit(
  person: [
    :name,
    :age,
    { address: [:street, :city] },
    { hobbies: [] }  # Empty array means permit any array values
  ]
)

Configuration Options

1
2
3
4
5
6
7
8
9
# In config/application.rb or an initializer

# Option 1: Permit all parameters (not recommended for production)
ActionController::Parameters.permit_all_parameters = true

# Option 2: Control behavior for unpermitted parameters
ActionController::Parameters.action_on_unpermitted_parameters = :log    # Default in development
ActionController::Parameters.action_on_unpermitted_parameters = :raise  # Raises exception
ActionController::Parameters.action_on_unpermitted_parameters = false   # No action

💡 Best Practice: Always explicitly permit parameters in your controllers. Never use permit_all_parameters = true in production, as it defeats the purpose of Strong Parameters.

Common Patterns

  1. Controller-level parameter method:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class PersonController < ApplicationController
  def create
    @person = Person.create(person_params)
  end

  private

  def person_params
    params.require(:person).permit(:name, :age, :email)
  end
end
  1. Handling arrays of objects:
1
2
3
4
5
6
7
# For nested forms with multiple records
params.permit(
  person: [
    :name,
    { phones: [:number, :type] }  # Allows array of phone objects
  ]
)
  1. Dynamic attributes:
1
2
3
4
5
6
7
8
9
# When attributes are dynamic
def permitted_params
  params.permit(:name, *custom_fields)
end

def custom_fields
  # Generate list of allowed fields dynamically
  current_user.allowed_fields
end

Filters and Callbacks

Controllers in Rails provide powerful filter mechanisms to run code before, after, or around controller actions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ApplicationController < ActionController::Base
  before_action :require_login
  after_action :log_action
  around_action :wrap_in_transaction

  private

  def require_login
    unless current_user
      flash[:error] = "You must be logged in"
      redirect_to login_path
    end
  end

  def log_action
    Rails.logger.info "Action completed by #{current_user&.email}"
  end

  def wrap_in_transaction
    ActiveRecord::Base.transaction do
      yield  # This yields to the actual controller action
    end
  end
end

💡 Tip: Use skip_before_action to skip filters for specific actions:

1
skip_before_action :require_login, only: [:index, :show]

Models

Active Record Basics

Rails models use Active Record to map database tables to Ruby objects.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class User < ApplicationRecord
  # Basic CRUD operations
  user = User.new(name: "John")
  user.save                  # INSERT
  user.name = "Jane"
  user.save                  # UPDATE
  user.destroy              # DELETE

  # Querying
  User.find(1)              # Find by primary key
  User.find_by(email: "example@email.com")
  User.where(active: true)
  User.order(created_at: :desc)

  # Scopes - reusable queries
  scope :active, -> { where(active: true) }
  scope :recent, -> { order(created_at: :desc) }
end

Validations

Rails provides rich validation capabilities for models:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Product < ApplicationRecord
  # Presence validations
  validates :name, presence: true
  validates :price, presence: true, numericality: { greater_than: 0 }

  # Format validations
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }

  # Custom validations
  validate :price_must_be_reasonable

  private

  def price_must_be_reasonable
    if price.present? && price > 1000000
      errors.add(:price, "is too high")
    end
  end
end

# Usage
product = Product.new
product.valid?  # => false
product.errors.full_messages  # => ["Name can't be blank", "Price can't be blank"]

Associations

Rails makes it easy to define relationships between models:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class User < ApplicationRecord
  # One-to-many relationship
  has_many :posts, dependent: :destroy
  has_many :comments

  # Many-to-many relationship
  has_many :followed_users, through: :relationships, source: :followed
  has_many :followers, through: :reverse_relationships

  # One-to-one relationship
  has_one :profile

  # Polymorphic association
  has_many :pictures, as: :imageable
end

class Post < ApplicationRecord
  belongs_to :user
  has_many :comments

  # Rich join tables
  has_many :post_categories
  has_many :categories, through: :post_categories
end

Views

ERB Templates

Rails uses ERB (Embedded Ruby) for view templates:

<!-- app/views/users/show.html.erb -->
<div class="user-profile">
  <% # Ruby code that doesn't output anything %>
  <% if @user.profile.present? %>

    <%# Ruby code that outputs something %>
    <h1><%= @user.name %></h1>

    <%# Safe HTML output %>
    <%= render 'shared/user_info', user: @user %>

    <%# Raw HTML output (be careful!) %>
    <%== @user.profile.html_bio %>
  <% end %>
</div>

Helpers

View helpers keep your templates clean and DRY:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# app/helpers/application_helper.rb
module ApplicationHelper
  def format_date(date)
    date.strftime("%B %d, %Y")
  end

  def avatar_for(user, size: 80)
    if user.avatar.attached?
      image_tag user.avatar.variant(resize: "#{size}x#{size}")
    else
      image_tag "default_avatar.png", size: "#{size}x#{size}"
    end
  end
end

# Usage in view
<%= format_date(@user.created_at) %>
<%= avatar_for(@user, size: 100) %>

Layouts and Partials

Rails encourages DRY views through layouts and partials:

<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield :title %> | MyApp</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag 'application' %>
  </head>
  <body>
    <%= render 'shared/header' %>

    <div class="container">
      <%= yield %>
    </div>

    <%= render 'shared/footer' %>
  </body>
</html>

<!-- app/views/shared/_header.html.erb -->
<header>
  <nav>
    <%= link_to 'Home', root_path %>
    <%= render 'shared/user_menu' if current_user %>
  </nav>
</header>

Routing

RESTful Routes

Rails promotes RESTful routing by default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# config/routes.rb
Rails.application.routes.draw do
  # Basic RESTful routes
  resources :posts do
    resources :comments  # Nested resources
  end

  # Customizing routes
  resources :users do
    member do
      get 'profile'
      patch 'update_profile'
    end

    collection do
      get 'search'
    end
  end

  # Namespace for API
  namespace :api do
    namespace :v1 do
      resources :posts, only: [:index, :show]
    end
  end
end

Custom Routes

Sometimes you need custom routes beyond REST:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Rails.application.routes.draw do
  # Custom member routes
  resources :posts do
    member do
      post 'publish'
      delete 'unpublish'
    end
  end

  # Custom collection routes
  resources :users do
    collection do
      get 'search'
      get 'export'
    end
  end

  # Singular resource
  resource :profile

  # Custom routes with constraints
  get 'posts/:year/:month/:day', to: 'posts#by_date',
    constraints: {
      year: /\d{4}/,
      month: /\d{1,2}/,
      day: /\d{1,2}/
    }
end

Additional Features

Concerns

Concerns allow you to share common code between models and controllers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# app/models/concerns/searchable.rb
module Searchable
  extend ActiveSupport::Concern

  included do
    scope :search, ->(query) {
      where("name LIKE ?", "%#{query}%")
    }
  end

  def search_rank
    # Custom search ranking logic
  end
end

# Usage in model
class Product < ApplicationRecord
  include Searchable
end

Mailers

Rails provides a powerful mailer system:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  def welcome_email
    @user = params[:user]
    @url = new_user_session_url

    attachments['welcome.pdf'] = WelcomePdf.new(@user).render

    mail(
      to: @user.email,
      subject: "Welcome to Our App!"
    )
  end
end

# app/views/user_mailer/welcome_email.html.erb
<h1>Welcome, <%= @user.name %>!</h1>
<p>You can login at: <%= @url %></p>

# Sending mail
UserMailer.with(user: @user).welcome_email.deliver_later

Active Storage

Rails makes file handling easy with Active Storage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class User < ApplicationRecord
  # Single file attachment
  has_one_attached :avatar

  # Multiple file attachments
  has_many_attached :documents

  # Validations for attachments
  validates :avatar, content_type: ['image/png', 'image/jpeg'],
                    size: { less_than: 5.megabytes }
end

# Usage in controller
def update
  if @user.update(user_params)
    redirect_to @user, notice: 'Profile updated!'
  else
    render :edit
  end
end

private

def user_params
  params.require(:user).permit(:name, :email, :avatar)
end

# Usage in view
<%= form_with(model: @user) do |form| %>
  <%= form.file_field :avatar %>
  <%= image_tag @user.avatar if @user.avatar.attached? %>
<% end  %>

💡 Best Practice: Always validate file types and sizes to prevent security issues and ensure good performance.


🎉 This guide covers the essential Rails features that make MVC implementation powerful and elegant. Remember that Rails follows convention over configuration, so following these patterns will make your development experience smoother and more productive!