Serving Static Files in Ruby
This is the fourth article in the series where we build a simple, yet complete web app in plain Ruby, without using Rails to better understand how Rails works. In this article, we'll learn how to serve static files in plain Ruby, without using Rails.
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::Staticmiddleware, 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.
💡 You might have heard of the separation of concerns principle. This is what it means in the simplest form.
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.pwdmethod returns the path to the current working directory of this process as a string. - The
File.joinmethod 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
.htmlorhtml.erbfile 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::Reloadermiddleware), 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.
😣 It's not working!
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::Staticmiddleware 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::Filesobject. 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:
- The Definitive Guide to Rack for Rails Developers
- Middleware in Rails: What It Is, How It Works, and Examples
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
paramsobject - 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!!
Sign up for my newsletter
Let's learn to become better developers.