When I first started learning Rails, one of the patterns that I thought was pure 'magic' was how you could access a controller's instance variables in the views.
# controllers/posts_controller.rb
class PostsController
def show
@post = Post.find(1)
end
end
# views/posts/show.html.erb
<%= @post.title %>
For a long time, I didn't understand how this would work behind the scenes. I tried reading the source code a few times without success. I just wasn't familiar enough with Ruby's advanced metaprogramming techniques.
However, as part of the series on building a web application in Ruby without Rails, I tried to come up with a way to implement the above feature myself. And just last night, I got it working. So, in the fifth article in the series, we'll learn one way to implement the Rails views pattern, where a controller's instance variables are accessible in the view, in pure Ruby.
This post won't show how Rails actually implements the views behind the scenes. I'm only trying to mimic the external Rails API, i.e. making controller instance variables available in views.
I still plan to read the Rails source code to really understand how the views work under the hood (watch out for a blog post on that soon).
However, I still wanted to show what a simplest views implementation could look like, to de-mystify some of the magic and give an intuitive understanding of the data handover from controllers to views. Personally, I find these projects a lot of fun and I always learn a bunch of Ruby in the process. I hope you do, too.
If you haven't read the previous posts in the series, this one might not make any sense. You can read the earlier articles here: Let's Build a Webapp in Ruby without Rails.
Alright, enough rambling. Let's get started.
As things stand now, our controllers are returning a plain text response from the action method. The router calls the controller's action to get this plain text response and returns it to the browser.
require_relative 'application_controller'
class ArticlesController < ApplicationController
def index
'All Articles'
end
end
To make it work like Rails, we'll want to assign this response to an instance variable:
require_relative 'application_controller'
class ArticlesController < ApplicationController
def index
@title = 'All Articles'
end
end
Then, the instance variables @title
will be used by the corresponding view, just like Rails.
<%# views/articles/index.html.erb %>
<h1>
<%= @title %>
</h1>
How should we go about this?
Solve a simpler problem first
Before trying to implement the above feature, let's try to solve a different, simpler problem. How can we access instance variables in a string template?
First, read this post that introduces the concept of binding in Ruby.

The basic idea behind binding is to store the current context in an object for later use. The Kernel#binding
method returns the current binding object. Think of this binding object as a wrapper that encapsulates the current programming environment, i.e. variables, methods, and even the self
object.
However, the concept of binding also extends to the instance variables of a class. That means if you get the binding
in the scope of an instance of a class, it contains the instance variables which you can use in an ERB template. The following example will make it clear.
require 'erb'
class Person
def initialize
@name = 'Akshay'
@age = 30
end
def get_binding
binding
end
end
ak = Person.new
erb = ERB.new 'My name is <%= @name %> and I am <%= @age %> years old'
puts erb.result(ak.get_binding)
# Output
# ======
# My name is Akshay and I am 30 years old
In the above code, the Person#get_binding
method returns the binding of an instance of the Person
class. When we call ak.get_binding
, it returns the binding of the instance ak
and contains the instance variables @name
and @age
, which the ERB uses to fill the string template.
Now that you understand how a template can access the instance variables of a class, let's go back to the original problem. We'll implement it in three simple steps.
Step One: Refactor the Router
This is what we have in the Router's get
method. To recap, given the path articles/index
, we build an instance of a controller ArticlesController
and dynamically call the index
method on it to get the plain-text response.
def get(path, &blk)
if blk
@routes[path] = blk
else
if path.include? '/' # 'articles/index'
controller, action = path.split('/') # 'articles', 'index'
controller_klass_name = controller.capitalize + 'Controller' # 'ArticlesController'
controller_klass = Object.const_get(controller_klass_name) # ArticlesController
@routes[path.prepend('/')] = ->(env) {
controller_klass.new(env).send(action.to_sym) # ArticlesController.new(env).index
}
end
end
end
Before we make any changes, let's fix a mistake I made in the previous post.
String#prepend
method modifies the original string, which we don't want. Hence, I'll replace it with '/' + path
so path
remains unchanged. This will also let us move the path-splitting and controller-building code inside the block, avoiding unnecessary work at the time of generating the routes.Here's the revised get
method. I also took the opportunity to rename a few variables and extract the controller-building method to keep it simple and concise.
def get(path, &blk)
if blk
@routes[path] = blk
else
if path.include? '/' # 'articles/index'
@routes['/' + path] = ->(env) {
controller_name, action_name = path.split('/') # 'articles', 'index'
controller_klass = constantize(controller_name) # ArticlesController
controller_klass.new(env).send(action_name.to_sym) # ArticlesController.new(env).index
}
end
end
end
private
# input: 'articles'
# output: ArticlesController
def constantize(name)
controller_klass_name = name.capitalize + 'Controller' # "ArticlesController" (a string)
Object.const_get(controller_klass_name) # ArticlesController (a class)
end
So far, we haven't changed any functionality yet.
Step Two: Add a render Method on the Controller
Let's create a new method called render
on the ApplicationController
class. We're adding it to the base controller class so that all controller classes will inherit it.
require 'erb'
class ApplicationController
attr_reader :env
def initialize(env)
@env = env
end
# new method
def render(view_template)
erb_template = ERB.new File.read(view_template)
erb_template.result(get_binding)
end
# new method
def get_binding
binding
end
end
The new code does the same thing that we saw in our simplified example above. There're three important things to note here:
- It reads the string template from a file called
view_template
, which we'll learn more about later. - Then it calls the
get_binding
method to get the binding in the context of the controller class, making all the instance variables available to the binding. - Finally, it renders the response by calling the
result
method on the ERB template and passing the binding.
If you're not following me, don't worry. I promise everything will start making sense very soon!
Step Three: Render from Router
We're almost done. The reason we added the render
method to the controller class is to call it from the router, once the instance variables are set.
Modify the router's get
method so it looks like this.
def get(path, &blk)
if blk
@routes[path] = blk
else
if path.include? '/' # 'articles/index'
@routes['/' + path] = ->(env) {
controller_name, action_name = path.split('/') # 'articles', 'index'
controller_klass = constantize(controller_name) # ArticlesController
controller = controller_klass.new(env) # controller = ArticlesController.new(env)
controller.send(action_name.to_sym) # controller.index
controller.render("views/#{controller_name}/#{action_name}.html.erb")
}
end
end
end
The most important point is that we've separated the action-calling and response-rendering mechanisms.
Before, we were simply calling the action method on the controller and returning whatever response the action method returned.
Now, we first call the action method so that it gets a chance to set the instance variables. Then, we call the render
method we just created, and pass it the name of the dynamically generated view template views/articles/index.html.erb
following Rails conventions.
Inside the render
method, the instance variables are already set, since it's the same controller instance. Hence it can render the view template without any problem.
We now have all the pieces in place. The only thing remaining is to use them. Let's update the controller so it sets the instance variables and add a view template that uses the instance variables.
Wrap up: Update the Controller and Create the View Template
Modify the ArticlesController
class, so that the index
action only sets the instance variables, instead of returning the response.
class ArticlesController < ApplicationController
def index
@title = 'All Articles'
end
end
Next, add the view template called index.html.erb
inside a newly created views/articles
directory.
<html>
<head>
<title>Application</title>
<link rel="stylesheet" href="/public/style.css">
<meta charset="utf-8">
</head>
<body>
<main>
<h1>
<%= @title %>
</h1>
</main>
</body>
</html>
Don't worry, we'll extract all the layout code in a separate template in a future post.
That's it, we're done. Now restart the application and visit localhost:9292/articles/index
in the browser. You should see the following page:

Any new instance variables you set in the controller's action method will be available to use in the view.
That's a wrap. In the upcoming posts, we'll:
- Implement models just like Rails!
- Improve the project structure and organization
- Add unit tests
- Handle errors and logging
- Process form inputs along with query strings into a
params
object - Connect to the database to store and fetch data
- Add middleware to handle specific tasks like authentication
- and much more...
If those sound interesting to you, consider subscribing to the blog.
Trust me, it's going to be a lot of fun, so stay tuned!!
I hope you liked this article and you learned something new.
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.