In the previous article, we created a simple-yet-complete web application in Ruby without using Rails. All it did was return a "Hello World" response back to the browser. In this post, we will improve our code and make it more Rails-like with the following enhancements:
- serve static files like stylesheets and images using the
Rack::Static
middleware - separate the views from the application logic, and
- generate response HTML dynamically using ERB
By the end, you'll have a pretty good understanding of how Rails dynamically generates HTML views.
Read the first article here:

Separation of Concerns
As things stand now, our application mixes the logic and the views together in a single file. Although there's no custom 'application logic' here, you can see the response HTML with h1
tags mixed in the Ruby script.
class App
def call(env)
headers = { 'Content-Type' => 'text/html' }
# This is view-specific code
response = ['<h1>Hello World!</h1>']
[200, headers, response]
end
end
This is 'generally' not considered good practice in software development, as it tightly couples the logic and view together (however, some folks (see: React) may have differing opinions). You can't change one of them without understanding or affecting the other.
Hence, the first thing we'll do is separate the application logic and the views. The benefit is that you can change the view without worrying about the logic, and vice versa.
Separate Views from Application
We will separate the view from the application logic by moving the response HTML out of the app.rb
to a file named index.html
under the views
directory, just like Rails.
<!-- views/index.html -->
<h1>Hello World!</h1>
Now update the app.rb
to read the contents of this file to build the response. We will use the File#read
method to read the file.
class App
def call(env)
headers = { 'Content-Type' => 'text/html' }
response_html = File.read 'views/index.html'
[200, headers, [response_html]]
end
end
Since File.read
returns the HTML contents, we need to wrap the response_html
into square brackets, as rack response expects an Array.
There are three benefits to separating the view from the application.
- The view gets its own
.html
orhtml.erb
file with the benefits that come with it, like IntelliSense and code organization. - Anytime you change the view, the application code picks it up automatically (even without the
Rack::Reloader
middleware), and they can vary on their own pace. - The biggest benefit is that the programmer and the designer can work on the Ruby and HTML code separately, without stepping over each others' toes. I think this was the major benefit Rails introduced in 2004, which was quite a big deal back then.
Refresh the browser to verify that everything is still working.
Let's Make It Pretty with CSS!
Have you noticed that our application is very plain-looking? Let's add some style.
First, we will standardize the index.html
by adding a proper HTML structure.
<html>
<head>
<title>Application</title>
<link rel="stylesheet" href="/public/style.css">
<meta charset="utf-8">
</head>
<body>
<main>
<h1>Hello World!</h1>
</main>
</body>
</html>
Reloading the browser shouldn't show any difference, except the nice title for the tab. Also, notice that we added the link to a public/style.css
file under the <head>
tag.
Let's create a new public
directory with the following style.css
file in it.
/* weby/public/style.css */
main {
width: 600px;
margin: 1em auto;
font-family: sans-serif;
}
Now reload the page.
Our page looks the same, and none of the styles are getting applied.
Before proceeding, can you guess why it's not working?
Let's inspect the response in the DevTools window. You can see that even though it's requesting the style.css
file, the response shows contents from the index.html
file, instead of the CSS we want.

What's going on?
The reason is that we are still serving the contents of the index.html
file when the request for style.css
arrives, as we don't have any logic to differentiate the requests.
class App
def call(env)
headers = { 'Content-Type' => 'text/html' }
response_html = File.read('views/index.html')
# same response, i.e. contents of index.html
# is sent no matter the request
[200, headers, [response_html]]
end
end
For any request our application receives, it's sending the same response. Hence it ignores the actual contents of the style.css
file.
Let's fix it.
Serving Static Files
When a request for style.css
arrives, we want to serve the contents of the style.css
file. Additionally, we want to serve style.css
as it is, without inserting any dynamic content to it.
The simplest solution would be to check the request path and return the appropriate view.
class App
def call(env)
headers = { 'Content-Type' => 'text/html' }
if env['REQUEST_PATH'].end_with?('.css')
response = File.read('public/style.css')
else
response = File.read('views/index.html')
end
[200, headers, [response]]
end
end
However, you can see it can get quite cumbersome as we add more stylesheets and images. It would be nice if there was a declarative way to specify all the static files we'd like to serve from a common folder. The good news is that there's an existing solution to solve this exact problem.
Let's see how we can use the Rack::Static
middleware to accomplish this.
According to the documentation,
TheRack::Static
middleware intercepts requests for static files (javascript files, images, stylesheets, etc) based on the url prefixes or route mappings passed in the options, and serves them using aRack::Files
object. This allows a Rack stack to serve both static and dynamic content.
Let's update the config.ru
file to include the Rack::Static
middleware.
require 'rack'
require_relative './app'
# Reload source after change
use Rack::Reloader, 0
# Serve all requests beginning with /public
# from the "public" folder
use Rack::Static, urls: ['/public', "/favicon.ico"]
run App.new
This tells the static middleware to serve all requests beginning with /public
from the "public" directory and to serve the favicon.ico
image from the current weby
directory. You can find a sample favicon.ico
file on GitHub.
That's it. Nothing needs to change in the app.rb
file, and it can remain as it is.
class App
def call(env)
headers = { 'Content-Type' => 'text/html' }
response_html = File.read('views/index.html')
[200, headers, [response_html]]
end
end
Now you might be wondering how this middleware works.
In short, it intercepts the incoming HTTP request before it even hits our application. Then it checks the request path to see if it matches the pre-configured pattern, i.e. /public
and serves the required file from this directory. Our application remains blissfully unaware that a request for a static file was even made. Pretty cool!
To learn more about Rack and the concept of middleware, check out the following articles:


Now restart the Puma server and reload the page. You can verify in the DevTools that our application sends the correct CSS this time, and our styles are getting applied. The favicon image is also getting loaded as expected. Woohoo!

Dynamically Rendering Views
As of now, our view is a static HTML. That means each time the page loads, our application renders the same HTML, no matter what.
We want a dynamic view, i.e., a view that can embed variables in it, to render a different page each time you reload the browser. For this, we'll need a view template.
What's a view template?
It's simply a file with predefined slots which will be filled dynamically in the future. Let's convert our index.html
to a view template.
Here's how we'll convert the static view into a view template.
- Change the name of the
index.html
file toindex.html.erb
- Update the
<h1>
tag to use atitle
variable instead of a hard-coded string.
<!-- views/index.html.erb -->
<html>
<head>
<title>Application</title>
<link rel="stylesheet" href="/public/style.css">
<meta charset="utf-8">
</head>
<body>
<main>
<h1>
<%= title %> <!-- Note the % -->
</h1>
</main>
</body>
</html>
The above view template will insert the value of the title
variable to generate the final HTML view that will be shown to the user.
Within an ERB template, Ruby code can be included using both<% %>
and<%= %>
tags. The<% %>
tags are used to execute Ruby code that does not return anything, such as conditions, loops, or blocks, and the<%= %>
tags are used when you want output.
For learning more about theerb
syntax, refer Rails guides.
The next question is, how to pass this title
variable to the view from the app.rb
file?
For this, we will use the ERB gem. It allows you to add any Ruby code to a plain text document for dynamically generating a new document. I recommend reading its documentation to learn more.
Here's a simple example that shows how it works:
require 'erb'
name = 'Akshay'
age = 30
erb = ERB.new 'My name is <%= name %> and I am <%= age %> years old'
puts erb.result(binding)
# Output:
#
# "My name is Akshay and I am 30 years old"
There're three things going on in the above example:
- After requiring the
ERB
gem and creating a few variables, we create an instance of theERB
class with a template string. - We create a
Binding
object by calling theKernel#binding
method. Think of this binding object as a wrapper that includes the current programming environment with variables likename
andage
, methods, and even theself
object. The basic idea is to store the current context in an object for later use. - The
result
method on theerb
object uses this binding object and the variables defined in that binding to replace the slots in the template string, generating the final string printed above.
Let's use the above structure to render the template from the app.rb
file:
require 'erb'
class App
def call(env)
headers = { 'Content-Type' => 'text/html' }
title = 'Ruby on Rails'
erb = ERB.new(html_template)
response_html = erb.result(binding)
[200, headers, [response_html]]
end
def html_template
File.read 'views/index.html.erb'
end
end
Now reload the browser. If everything worked, you should see our (new) title Ruby on Rails on the page.
Our application is generating views on the fly 🔥
Not So Dynamic, Yet!
Now you might have questions about the dynamic nature of the above code, as we're still using the hard-coded string Ruby with Rails
. You're correct, but the important part is that we are fetching the value from the title
variable. This value can come from anywhere, which is the part that makes it dynamic.
To illustrate this, let's tweak our code to read the title from the query string instead of a hard-coded string.
We will add a new method named get_title
which will parse the env
hash to fetch the query string, and return its value.
require 'erb'
class App
def call(env)
headers = { 'Content-Type' => 'text/html' }
title = get_title(env) # new code here
template = ERB.new(template_html)
response_html = template.result(binding)
[200, headers, [response_html]]
end
def get_title(env)
query = env['QUERY_STRING'] # "title=ruby"
values = query.split('=') # ["title", "ruby"]
values[1] # ruby
end
def template_html
File.read 'views/index.html.erb'
end
end
Now append ?title=whatever
to the existing URL and refresh the browser. You should see the browser display whatever text you typed in the URL.

Congratulations, you now have a fully dynamic web application. We could improve it further, but I'll stop here as this post has already crossed 2000 words and I need to go to bed.
In future articles in this series, we'll explore the following topics together.
- Building a custom router to handle requests based on URL patterns
- Improving the project structure and organization
- Introducing the concepts of controllers and models
- Handle errors and logging
- Process form inputs along with query strings into a
params
object - Connect to the database to store and fetch data
- Adding 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!!
That's a wrap. 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.