Building a Web Application Without Rails : Serving Static Files

June 26, 2022   • no-rails-app

This is the second post in the series on building a web application without using Rails. In the last post, we set up a very basic project structure for our web application which returns a hard-coded string response directly from the Ruby code.

class App
  def call(env)
    headers = { 'Content-Type' => 'text/html' }

    return [200, headers, ['favicon']] if env['PATH_INFO'] == '/favicon.ico'

    response = ['<h1>Greetings from Rack!!</h1>']
    [200, headers, response]
  end
end

In this post, we will separate the view from the application logic, and serve static files using the Rack::Static middleware. If you prefer to watch a video instead, scroll to the bottom of this post.


Our application mixes the logic and the views together in a single file. This is not a good practice in software development, as it tightly couples them together. You can’t change one of them without understanding or affecting the other.

We want to separate the application logic and the views. The benefit is that you can change the view without worrying about the logic, and vice versa. You might have heard of the separation of concerns principle, this is what it means in the simplest form.

Let’s fix this.

Set up Views

We will separate the view from the application logic by moving the response HTML out of the app.rb and to its own views directory, in a file named index.html .

<!-- views/index.html -->

<h1>Greetings from Rack!!</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' }

    return [200, headers, ['favicon']] if env['PATH_INFO'] == '/favicon.ico'

    response_html = File.read 'views/index.html'
    [200, headers, [response_html]]
  end
end

Refresh the browser to verify that everything is still working.

Make It Pretty

Have you noticed that our application is very plain-looking? Let’s add some style to it.

First, we will standardize the index.html by adding a proper HTML structure. Notice that we are referring to a style.css file in the public directory.

<html>
  <head>
    <title>Application</title>
    <link rel="stylesheet" href="/public/style.css">
    <meta charset="utf-8">
  </head>

  <body>
    <main>
      <h1>Greetings from Rack</h1>
    </main>
  </body>
</html> 

Now add a new public directory and add a style.css file in it.

/* /public/style.css */

main {
  width: 600px;
  margin: 1em auto;
  font-family: sans-serif;
}

Now reload the page. But our page looks the same, and none of the styles are getting applied. If we check response in the devtools, it’s loading the style.css file, but the contents are from the index.html file. How’s that possible?

CSS

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 them, like we do for a favicon request. Hence it serves the index.html, ignoring the actual contents of the style.css file.

Serve Static Files

When a request for style.css arrives, we want to serve the contents of the style.css file. For that, we will use the Rack::Static middleware.

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'

use Rack::Reloader, 0

# Serve all requests beginning with /public from the "public" folder 
use Rack::Static, urls: ['/public']

run App.new

Restart the Puma server and reload the page. The CSS should be applied now.

Style CSS

Congratulations, you are now serving static files for our application. In the next post, we will learn how to build dynamic views using erb templates, just like Rails does.