So far, we have created a simple-yet-complete web application in Ruby without using Rails with the support of routing and controllers. However, we are still rendering the contents of the view from our controller classes. For example:
class ArticlesController < ApplicationController
def index
'<h1>All Articles</h1>'
end
end
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, and - separate the views from the application logic
By the end, you'll have a pretty good understanding of how to serve static files using a middleware in Ruby.
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.
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.
Although it works, it's a good idea to separate the view from the controller. That way, we don't have to change the controller classes when the view needs to be updated, and vice-versa. Both the controller and view can evolve independently.
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 also change the logic without affecting the views.
Separate View 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 a newly created views
directory, just like Rails.
<!-- views/index.html -->
<h1>All Articles</h1>
Now update the articles_controller.rb
to read the contents of this file to build the response. We will use the File#read
method to read the file.
require_relative 'application_controller'
class ArticlesController < ApplicationController
def index
index_file = File.join(Dir.pwd, "views", "index.html")
File.read(index_file)
end
end
A few things to note here:
- The
Dir.pwd
method returns the path to the current working directory of this process as a string. - The
File.join
method returns a new string formed by joining the strings using"/"
.
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. 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 to make it look pretty.
First, let's 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>All Articles</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.
Notice the response: "no route found for /public/style.css"
Since we haven't added a route for the style.css
fiIe, our application doesn't know which file to serve when the browser sends a request for it.
We could fix this by adding a new route, so that our application can serve the stylesheet.
However, you can see it can get quite cumbersome as we add more stylesheets and images. In addition, since a stylesheet is a static file, i.e. its content is not generated dynamically using Ruby.
It would be nice if there was a declarative way to specify all the static files we'd like to serve from a common directory, so our application doesn't have to worry about it.
The good news is that there's an existing solution to solve this exact problem.
Serving Static Files with Middleware
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 Rack::Static
middleware lets us accomplish this exact use case. 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']
run App.new
This tells the static middleware to serve all requests beginning with /public
from the "public" directory.
That's it. Nothing needs to change in either the app.rb
file or our controller.
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 😃
Now you might be wondering how this middleware works.
How Does Rack::Static Middleware Work?
First, it intercepts the incoming HTTP request before it even hits our application. Next, it checks the request path to see if it matches the pre-configured pattern, i.e. /public
. Finally, it 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:
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.
In the next lesson, we will learn how to use Rails-like dynamic views using the ERB gem.
Also, in the future articles in this series, we'll explore the following topics together.
- Improving the project structure and organization
- Introducing the concepts of 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...
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.