Building Rails Features the Idiomatic Way: MVC Flow, RESTful Routing, Views with ERB and Partials, and Model Associations

This chapter provides a cohesive, self-contained guide to building Rails features following conventions. It explains how the MVC request–response cycle works, how RESTful routes map to controller actions, how controllers pass data to views, how ERB templates (and partials) render dynamic HTML, how helpers like link_to and route helpers keep URLs maintainable, and how to design and implement model associations (with migrations, indexes, and nested routes). Throughout, we use a concrete ItemModel resource and a Units resource that belongs to ItemModel.

1) The Rails MVC Request–Response Cycle

Rails applications implement the Model–View–Controller (MVC) architectural pattern to separate concerns:

  • Models encapsulate data and business rules and interact with the database via Active Record.
  • Controllers coordinate requests: they retrieve or modify models, prepare data for the view, and choose what to render or where to redirect.
  • Views present data as HTML (or other formats), typically via ERB templates.

A typical request journey:

  1. The browser sends an HTTP request (for example, GET https://example.com/item_models/3).
  2. The Rails router parses the path and selects a controller and action according to routes defined in config/routes.rb (for /item_models/3, ItemModelsController#show).
  3. The controller action runs, using params to access request data (e.g., params[:id]) and loading data from models (ItemModel.find(params[:id])).
  4. The model executes the database query and returns a Ruby object.
  5. The controller exposes the data to the view via instance variables (e.g., @item_model) and renders the appropriate template.
  6. The view (ERB) produces HTML, which Rails returns as the HTTP response.

This flow enforces a clean separation of business logic, data access, and presentation, making applications maintainable and scalable.

2) REST, CRUD, and Rails Routing

Rails encourages RESTful design, organizing resources around standard CRUD operations:

  • Create: new and create
  • Read: index and show
  • Update: edit and update
  • Delete: destroy

2.1 Defining resourceful routes

The resources helper generates the canonical seven routes for a resource:

# config/routes.rb
Rails.application.routes.draw do
resources :item_models
end

If you need a subset:

resources :item_models, only: [:index, :show]

Inspect routes with:

bin/rails routes

This lists route helpers (prefix), HTTP verb, URL pattern, and controller#action. For ItemModel, you will see patterns like:

  • GET /item_models → item_models#index
  • GET /item_models/:id → item_models#show

The optional (.:format) allows serving different formats (HTML by default, JSON, etc.).

2.2 Route helpers and maintainable URLs

Rails creates path/url helpers from route names:

  • item_models_path → "/item_models"
  • item_model_path(3) → "/item_models/3"
  • item_model_path(@item_model) → "/item_models/:id" for the given instance

Use these helpers instead of hardcoded strings to keep links robust if routes change. Helpers are used in controllers and views, commonly with link_to.

2.3 Setting a root route

Choose a default entry point for the application:

Rails.application.routes.draw do
resources :item_models
root to: "item_models#index"
end

Visiting http://localhost:3000 now renders item_models#index.

3) Controllers, Actions, and params

A controller is a Ruby class inheriting from ApplicationController. Public methods are actions and are reached via routes.

Example:

class ItemModelsController < ApplicationController
def index
@item_models = ItemModel.all
end
def show
@item_model = ItemModel.find(params[:id])
end
end

Key points:

  • params is a Hash carrying route parameters, query strings, and form fields. For /item_models/3, params[:id] == "3".
  • Instance variables (e.g., @item_models, @item_model) are copied into the view context, making them available to templates.
  • Rails auto-renders the conventional template when you don’t call render or redirect_to explicitly (e.g., app/views/item_models/index.html.erb for index).

Development tip: Rails error pages often tell you exactly which view path is missing; carefully reading errors speeds up debugging. After renaming files or folders, a server restart can help if autoloading did not pick up the changes.

4) Views with ERB: Output vs Control Flow, Iteration, and Helpers

Rails uses ERB to embed Ruby in HTML.

  • Output tags: <%= ... %> evaluate and insert the result in HTML.
  • Control flow tags: <% ... %> run Ruby without producing output. Use for loops and conditionals.

Common pitfalls:

  • Forgetting the equals sign in <%= ... %> leads to no visible output.
  • Adding equals to a control structure tag (e.g., <%=%> around each) can produce unexpected output.

4.1 Index and show views

Rendering a list in index:

<!-- app/views/item_models/index.html.erb -->
<table>
<% @item_models.each do |item_model| %>
<tr>
<td><%= link_to item_model.name, item_model %></td>
<td><%= item_model.brand %></td>
</tr>
<% end %>
</table>

Rendering details in show:

<!-- app/views/item_models/show.html.erb -->
<h1><%= @item_model.name %></h1>
<p>Brand: <%= @item_model.brand %></p>

You can also use string interpolation inside an output tag:

<p><%= "#{item_model.brand}: #{item_model.name}" %></p>

4.2 View helpers: link_to and formatting

  • link_to(text, path_or_record) generates an anchor tag. Passing an Active Record object uses polymorphic URL helpers (e.g., item_model_path(item_model)).

Examples:

<%= link_to "List", item_models_path %>
<%= link_to item_model.name, item_model %>
<%= link_to item_model.name, item_model_path(item_model.id) %>

Additional useful helpers:

  • number_to_currency(1234567.89) → $1,234,567.89
  • pluralize(4, "unit") → 4 units
  • time_ago_in_words(@item_model.created_at) → e.g., "3 days"

These helpers keep view code concise and readable.

5) Partials: Reusing View Fragments and Rendering Collections

Partials are ERB files prefixed with an underscore that encapsulate reusable fragments (such as a table row or card). They reduce duplication and keep views clean.

Example partial to render one item:

<!-- app/views/item_models/_item_model.html.erb -->
<tr>
<td><%= link_to item_model.name, item_model %></td>
<td><%= item_model.brand %></td>
</tr>

Render a collection in index:

<!-- app/views/item_models/index.html.erb -->
<table>
<tbody>
<%= render @item_models %>
</tbody>
</table>

How it works:

  • render @collection infers the partial name from the model (for ItemModel, _item_model) and renders it for each element.
  • Each render call passes a local variable named after the model in snake_case (item_model) into the partial.

This pattern often reduces an index view to a single render line, improving maintainability and consistency.

6) Moving Presentation Logic into Models

To avoid duplicating formatting rules across views, define display-oriented methods in the model. Views then stay declarative.

Example:

# app/models/item_model.rb
class ItemModel < ApplicationRecord
def display_name
"#{brand}: #{name}"
end
end

Use in views:

<!-- app/views/item_models/_item_model.html.erb -->
<p><%= item_model.display_name %></p>
<!-- app/views/item_models/show.html.erb -->
<h2><%= @item_model.display_name %></h2>

This centralizes presentation logic, making future changes straightforward.

7) Implementing One-to-Many Associations: ItemModel has_many Units

Real applications model relationships. A common pattern is one-to-many: one ItemModel describes a product type, while many Units represent concrete, physical instances of that model.

7.1 Designing the database: foreign keys and indexes

In a one-to-many relationship, place the foreign key on the “many” side. For Units → ItemModel, add item_model_id to the units table.

Best practices:

  • Use Rails conventions for foreign keys: model_name_id (item_model_id).
  • Always index foreign key columns to speed up lookups.
  • Add foreign key constraints for data integrity.

Create the units table with a migration:

# db/migrate/XXXXXXXXXXXXXX_add_units_table.rb
class AddUnitsTable < ActiveRecord::Migration[7.0]
def change
create_table :units do |t|
t.string :asset_tag, null: false
t.references :item_model, null: false, foreign_key: true
t.timestamps
end
end
end

Alternatively, generate the model and migration in one command:

rails generate model Unit asset_tag:string item_model:references

Then run:

bin/rails db:migrate

Important: Once a migration has run, editing it has no effect; create a new migration for further changes. If you already had an item_model_id column without proper constraints, you can add them later:

class AddForeignKeyToUnits < ActiveRecord::Migration[7.0]
def change
add_foreign_key :units, :item_models
add_index :units, :item_model_id
end
end

7.2 Declaring associations in models

Define the relationship on both sides:

# app/models/item_model.rb
class ItemModel < ApplicationRecord
has_many :units
# Optionally: has_many :units, dependent: :nullify or :destroy (domain-specific)
end
# app/models/unit.rb
class Unit < ApplicationRecord
belongs_to :item_model
validates :asset_tag, presence: true
end

With this in place:

  • unit.item_model returns the parent ItemModel.
  • item_model.units returns an ActiveRecord::Relation of associated units and can be further scoped (where, order, etc.).

Be mindful of performance: deeply nested association traversals can cause N+1 queries. While advanced strategies are beyond this chapter’s scope, awareness helps you revisit design if pages trigger many queries.

7.3 Displaying associated data in views

Once associations are defined, showing related data becomes straightforward. For example, list Units in the ItemModel show page:

<!-- app/views/item_models/show.html.erb -->
<h1><%= @item_model.display_name %></h1>
<h2>Associated Units</h2>
<ul>
<% @item_model.units.each do |unit| %>
<li><%= unit.asset_tag %></li>
<% end %>
</ul>

If there are no units, the loop renders nothing—an acceptable and clear outcome.

8) Nested Routes for Associated Resources

URLs can reflect relationships through nested routes. For example, to list all units of an item model:

  • GET /item_models/3/units → “units belonging to item model 3”

Define nested routes:

# config/routes.rb
Rails.application.routes.draw do
resources :item_models do
resources :units
end
end

This generates helpers such as:

  • item_model_units_path(@item_model)
  • item_model_unit_path(@item_model, @unit)

In UnitsController, access the parent via params:

class UnitsController < ApplicationController
def index
@item_model = ItemModel.find(params[:item_model_id])
@units = @item_model.units
end
end

Recommendation: limit nesting to one level to avoid unwieldy URLs and complex controllers.

9) End-to-End Example: From routes to views

Routes:

resources :item_models
root to: "item_models#index"

Controller:

class ItemModelsController < ApplicationController
def index
@item_models = ItemModel.order(:name)
end
def show
@item_model = ItemModel.find(params[:id])
end
end

Model display logic:

class ItemModel < ApplicationRecord
def display_name
"#{brand}: #{name}"
end
end

Partial and views:

<!-- app/views/item_models/_item_model.html.erb -->
<tr>
<td><%= link_to item_model.name, item_model %></td>
<td><%= item_model.brand %></td>
</tr>
<!-- app/views/item_models/index.html.erb -->
<h1>Item Models</h1>
<table>
<thead>
<tr>
<th>Link</th>
<th>Brand</th>
</tr>
</thead>
<tbody>
<%= render @item_models %>
</tbody>
</table>
<p>
<%= link_to "Create new", new_item_model_path %> |
<%= link_to "Back to list", item_models_path %>
</p>
<!-- app/views/item_models/show.html.erb -->
<h1><%= @item_model.name %></h1>
<p>Brand: <%= @item_model.brand %></p>
<p>Price: <%= number_to_currency(@item_model.price) %></p>
<p>Created: <%= time_ago_in_words(@item_model.created_at) %> ago</p>
<p>
<%= link_to "Back to list", item_models_path %>
</p>

Units migration (example):

class AddUnitsTable < ActiveRecord::Migration[7.0]
def change
create_table :units do |t|
t.string :asset_tag, null: false
t.references :item_model, null: false, foreign_key: true
t.timestamps
end
end
end

Associations:

class ItemModel < ApplicationRecord
has_many :units, dependent: :nullify
end
class Unit < ApplicationRecord
belongs_to :item_model
end

10) Debugging and Common Pitfalls

  • No route matches [GET] "/path": The route is missing or the URL is mistyped. Define routes in config/routes.rb and confirm with bin/rails routes.
  • Unknown action or missing controller: The route points to an action you haven’t defined. Create the controller and method.
  • Template is missing or ActionController::UnknownFormat: Rails could not find the expected view template (e.g., app/views/item_models/show.html.erb). Ensure the correct folder (plural controller name) and file naming.
  • ActiveRecord::RecordNotFound: find(params[:id]) didn’t match a record. Check IDs and seed data.

Read error pages carefully: they often indicate precisely what Rails expected and where it looked.

11) Conventions, Naming, and Style Notes

  • Views folder names are plural to match the controller (app/views/item_models), not singular.
  • Ruby method names use snake_case (display_name, not DisplayName).
  • Prefer route helpers (item_models_path) or passing the object to link_to instead of hardcoding URLs.
  • Favor partials for repeated structures, and keep complex formatting or presentation rules in model methods or helpers to keep templates simple.

12) Why These Pieces Fit Together

  • RESTful routes standardize how users and code access resources, making navigation and linking predictable.
  • Controllers mediate between HTTP and domain logic, setting up instance variables for templates.
  • ERB enables clean composition of HTML with dynamic data. Output and control tags maintain clarity between content and logic.
  • Partials and helpers reduce duplication, enforce consistency, and keep views focused.
  • Associations encode domain relationships directly in both the database and code, enabling expressive, readable queries and templates.
  • Nested routes reflect real relationships in the URL structure when appropriate, enhancing clarity for users and developers.