Serving static files in Ruby

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.

7 min read

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:

  1. serve static files like stylesheets and images using the Rack::Static middleware, and
  2. 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.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.

  1. The view gets its own .html or html.erb file with the benefits that come with it, like IntelliSense and code organization.
  2. 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.
  3. 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,

The Rack::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 a Rack::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:

The Definitive Guide to Rack for Rails Developers
The word Rack actually refers to two things: a protocol and a gem. This article explains pretty much everything you need to know about Rack as a Rails developer. We will start by understanding the problem Rack solves and move to more advanced concepts like middleware and the Rack DSL.
Middleware in Rails: What It Is, How It Works, and Examples
In this post, We’ll learn about Rails middleware: what it is, why we need it, how it works, and why it’s so important. If you have a fuzzy understanding of middleware, this post will make it concrete. I’ll also show you how to create and test custom middleware for your Rails app.

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.