Rails MVC Pattern: How Models, Views, and Controllers Fit Together
Every Rails application follows the same underlying structure. Understanding that structure is the difference between writing code that works and understanding why it works. The pattern Rails uses is called MVC — Model, View, Controller — and it separates your application into three distinct layers, each with a single responsibility.
This tutorial walks through what MVC means in Rails, how a request travels through each layer, and how the pieces fit together with real code examples.
Intro Context
The Rails MVC pattern is easiest to understand when you treat it like a traffic system. A request comes in, the router decides where it should go, the controller coordinates the work, the model handles data, and the view turns the result into something a browser can display. That flow keeps application code organized without making any one layer do everything.
If you learn the pattern early, it becomes much easier to read existing Rails code because you know where to look for each responsibility. Models hold business rules, controllers manage request flow, and views focus on presentation. That split is the backbone of a lot of Rails code you will see in the wild.
The reason MVC matters is that it gives you a simple map for the codebase. Instead of hunting through one giant file for everything, you can usually predict where a piece of logic belongs. That makes debugging faster because a rendering issue points you toward the view, while a bad database lookup points you toward the model or controller.
TL;DR
- Models manage data and business rules.
- Controllers coordinate requests and responses.
- Views render what the user sees.
- Rails conventions connect the layers automatically, so naming matters.
- A request usually travels router → controller → model → controller → view.
what MVC actually means in Rails
MVC is an architectural pattern. In Rails, each layer has a specific job:
- Model — manages your data and the business rules around it. In Rails, this is powered by ActiveRecord, which maps your Ruby objects to database tables.
- View — renders the response. It takes data from the controller and turns it into HTML, JSON, or whatever format the user needs.
- Controller — coordinates the flow. It receives the incoming request, talks to the model, and hands data off to the view.
The key benefit is that each piece doesn’t need to know how the others work. Change your HTML without touching your database queries. Rework your business logic without rewriting your controller. Developers can work on different layers without stepping on each other’s toes.
That separation is what keeps Rails projects maintainable as they grow. Once you know which layer owns each responsibility, it is much easier to decide where a new method belongs and which file you should open first when something breaks.
the request lifecycle
Before diving into each layer, it helps to see the full picture. When a browser makes a request to a Rails application, here’s what happens:
Browser → Router → Controller → Model → Controller → View → Response
Let’s trace through a concrete example: GET /posts
- The Router receives the request and matches
GET /poststoPostsController#index. - The Controller action
indexruns. It callsPost.allon the model. - The Model queries the database and returns a collection of post objects.
- The Controller stores that collection in an instance variable:
@posts = Post.all. - The View renders
app/views/posts/index.html.erb, where@postsis available. - The finished HTML is sent back to the browser.
That full round-trip is the request lifecycle. Every request in Rails follows this path.
When you are reading a Rails app for the first time, the request lifecycle is a good mental checklist. Start with the URL, follow it through the router, identify the controller action, and then check whether the model or view is doing too much work. That habit keeps the architecture understandable because you can trace the data flow instead of guessing at it.
You do not need to memorize every middleware detail to be productive. It is enough to know that the router maps the URL, the controller gathers data, the model loads or saves records, and the view turns the response into HTML or JSON.
the model layer
The model is backed by ActiveRecord, which is an ORM: an Object-Relational Mapping layer. Instead of writing SQL, you work with Ruby objects; ActiveRecord handles the translation to your database.
Models define the structure of your data and the relationships between pieces of it.
This is also where you usually put validations, callbacks, and business rules that need to be reused across controllers. If the logic belongs to the data itself, the model is usually the right home for it.
class Post < ActiveRecord::Base
has_many :comments, dependent: :destroy
belongs_to :author, class_name: "User"
validates :title, presence: true, length: { minimum: 5 }
validates :slug, uniqueness: true
before_validation :generate_slug
private
def generate_slug
self.slug ||= title.parameterize
end
end
has_many and belongs_to are association declarations. They give you methods like post.comments, comment.post, and post.comments.create(...) without writing a line of SQL.
Validations run before the model saves to the database. If validation fails, save returns false and the record doesn’t persist. This keeps bad data out of your system at the application level.
Callbacks like before_validation let you hook into the object’s lifecycle. You can run logic before saving, after creating, around destroying — whatever you need.
Migrations handle your schema. They’re version-controlled Ruby files that describe changes to your tables. Each migration has a timestamp in its filename, which Rails uses to determine the order of execution. Running rails db:migrate applies any pending migrations and updates the schema, giving you a reproducible way to evolve the database alongside the application code:
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.string :title
t.text :body
t.timestamps
end
end
end
Run rails db:migrate and Rails creates the posts table with the columns you defined.
the controller layer
Controllers receive the HTTP request and coordinate everything else. A RESTful Rails controller typically exposes seven actions that correspond to the standard CRUD operations.
class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :destroy]
def index
@posts = Post.all
end
def show
# @post set by before_action
end
def new
@post = Post.new
end
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: "Post was successfully created."
else
render :new, status: :unprocessable_entity
end
end
def edit
# @post set by before_action
end
def update
if @post.update(post_params)
redirect_to @post, notice: "Post was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@post.destroy
redirect_to posts_url, notice: "Post was successfully deleted."
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :body, :slug)
end
end
before_action :set_post runs the set_post method before specific actions. It keeps repetitive code out of your actions. find(params[:id]) looks up the record by its primary key — the :id comes from the URL route like /posts/42.
The post_params method uses strong parameters to whitelist which fields can be submitted. require(:post) ensures the :post key is present in the request. permit(...) lists the allowed fields. Anything else is silently ignored. This protects against mass assignment vulnerabilities.
A common mistake is using redirect_to :new after a failed create. That tells the browser to make a new GET request, which loses all the form data the user submitted. The correct approach is render :new, which re-renders the form while keeping the user’s input intact.
Controllers should stay thin. Their job is to gather the right data, pick the right response, and hand the actual work off to models or helpers where possible.
the view layer
Views live in app/views/ and use ERB templates — HTML files with embedded Ruby. The view’s only job is to present data. It should never query the database or modify application state.
<%# app/views/posts/index.html.erb %>
<h1>All Posts</h1>
<%= link_to "New Post", new_post_path %>
<% @posts.each do |post| %>
<article>
<h2><%= link_to post.title, post %></h2>
<p><%= truncate(post.body, length: 100) %></p>
</article>
<% end %>
The two ERB syntaxes behave differently. <%= %> evaluates the expression and outputs the result into the HTML. <% %> evaluates without outputting — useful for control flow like each loops. Using <%= %> inside an <% %> block to output values within a loop is fine.
link_to, truncate, and pluralize are view helpers. Rails ships with a large set of helpers that handle common presentation tasks. You can also write your own.
Instance variables set in the controller are automatically available in the view. If your controller sets @posts, the view can access @posts directly. That’s the primary communication channel between the two layers.
Views should focus on formatting and presentation. If a view starts querying the database or deciding how to update records, that is usually a sign the logic belongs somewhere else.
What views should not do: query the database (Post.find(...)), modify params, or call redirect_to. Those responsibilities belong to the controller.
layouts and partials
Every page on a Rails site shares a common wrapper. That wrapper lives in a layout file:
app/views/layouts/application.html.erb
Partials are reusable template fragments. A _form.html.erb partial for a post might look like:
<%# app/views/posts/_form.html.erb %>
<%= form_with model: post do |f| %>
<% if post.errors.any? %>
<div class="errors">
<ul>
<% post.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :title %>
<%= f.text_field :title %>
</div>
<div>
<%= f.label :body %>
<%= f.text_area :body %>
</div>
<%= f.submit %>
<% end %>
Render it from any view with the render helper, passing the partial name as a symbol or string and any local variables as keyword arguments. Rails looks for the partial in the same directory as the calling view by default, or you can specify a full path like 'shared/form' for partials stored in a common location:
<%= render 'form', post: @post %>
The error check uses post.errors.any? to determine whether to display anything. If the model has validation errors, full_messages returns an array like ["Title is too short (minimum is 5 characters)", "Body can't be blank"]. This pattern works with any form built using form_with, form_for, or form_tag. Note that f.error_message_on is not a standard FormBuilder method; it doesn’t exist in Rails and would raise a NoMethodError.
Partials keep repeated markup in one place, while layouts keep the overall page shell consistent. Together they let you move fast without copying the same HTML into every template. When you need to reuse a form, a card, or a list item across multiple pages, extracting it into a partial is the simplest way to keep the markup DRY while still giving each view control over how the partial is called.
conventions over configuration
Rails reduces boilerplate by assuming conventions. A PostsController automatically looks for the Post model, the posts table in the database, and views in app/views/posts/. You don’t have to configure any of that explicitly.
| Convention | Example |
|---|---|
| Controller name | PostsController → /posts |
| Model name | Post (singular, CamelCase) |
| Table name | posts (plural, snake_case) |
| Primary key | id |
| Foreign key | post_id |
| View folder | app/views/posts/ |
When you follow the conventions, Rails connects all the pieces automatically. When you need to deviate, you can override them — but most of the time, the conventions handle everything.
Forward Link
If MVC now feels more concrete, the next step is to see how the pieces are created and changed in a real application. The Rails migrations tutorial shows how schema changes support the model layer, while the Rails routing tutorial shows how requests reach the controller in the first place. Looking at both sides helps MVC click because you can connect the architecture to real code paths.
See Also
- Rails Getting Started — set up a Rails project from scratch
- Rails ActiveRecord Basics — deeper look at the model layer
- Rails Routing — how the router matches requests to controllers