The Definitive Guide to Rack for Rails Developers

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.

12 min read

P.S. I originally published this post last year, when I was just starting to learn Rails. Since then, I've learned a few more things and hence decided to revise, polish, and re-publish it. Also, a ton of new subscribers have joined the email list, and I thought you all might find it helpful.

You’ve been around in the Rails world for a while. You know your way around rails. But you keep hearing the word ‘Rack’ and don’t really understand what it is or what it does for you.

You try to read the documentation on the Rack Github repository or the Rails on Rack guides, but the only thing it does is add to the confusion. Everyone keeps saying that it provides a minimal, modular, and adaptable interface for building web applications. But what the heck does that really mean?

If there’s a list of topics that confuses most new Rails developers, Rack is definitely up there at the top. When I started learning Ruby and Rails last year, Rack took a really long time to wrap my head around. If you are in the same boat, fear not. In this article, I will try to explain pretty much everything that you need to know about Rack as a Ruby and Rails developer.

What You'll Learn:

It’s quite a long article, but if you stick to the end, you will have a much better understanding of Rack. We'll cover the Rack protocol, the Rack gem, the middleware toolbox, the Rack DSL, and much more. This is the day when you learned Rack.

Sounds good? Let's get started.

I strongly believe that to understand any solution, we first need to understand the problem. Hence, before we try to understand the theory behind Rack, let’s try to understand the basic problem it's trying to solve, by building a very simple web application in Ruby using different web servers, such as Puma, Thin, and Unicorn.

Making Ruby Code Talk to Web Servers

Let's build a simple web application in plain Ruby.

Create a new directory and add a file named config.ru. Don’t worry, it’s just a regular Ruby file with a different extension (it’s short for rackup, but let’s ignore that for now). All Rails apps have a config.ru file in the root directory.

mkdir web
cd web 

touch config.ru

Now add the following code in this file, which creates the most basic web application you have seen. Don’t worry about the run method at the bottom for now. We will return to it later.

# config.ru

class App
   def call(env)
     headers = { 'Content-Type' => 'text/html' }
     
     response = ['<h1>Greetings from Rack!!</h1>']
     
     [200, headers, response]
   end
end

run App.new

This is our very own web application that returns a simple response. Now let’s try to run it in the browser. For that, we will need a web server, a piece of software that accepts HTTP requests and returns HTTP response.

I will use Puma, which is the web server that Ruby on Rails ships with.

Puma

Install and launch Puma using the following commands from the web directory:

➜ gem install puma 

➜ puma
Puma starting in single mode...
* Puma version: 5.6.4 (ruby 3.1.0-p0) ("Birdie's Version")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 32026
* Listening on http://0.0.0.0:9292
Use Ctrl-C to stop

By default, Puma looks for a config.ru file in the same directory, and uses that to launch our web application.

Now our server is up and running. Point your browser to http://localhost:9292 and you will see this:

Stop the server using Ctrl + c command. At this point, we have a very simple web application up and running using the Puma web server.

Now let’s run this application using a different server. We’ll use Thin, a small, simple, and fast web server.

Thin

Install and launch the Thin web server using the following commands.

gem install thin
thin start

The Thin server is up and running and serving our application. Point your browser to http://localhost:3000 and you should see the same web page that we saw earlier.

Let’s try one last time to use a different web server. This time we’ll use Unicorn, an old web server that used to be popular in the Rails community.

Unicorn

Install and launch the Unicorn web server using the following commands.

gem install unicorn
unicorn

Point your browser to http://localhost:8080. Again, you should see the same web page.

What's the Point?

A few questions you might have right now:

  1. How did all of these web servers know how to run our application?
  2. Why didn’t we have to change even a single line in our application to make it work with a different server?

The answer is that our application follows the Rack protocol, and they all are rack-compliant web servers.

Okay, but what does that mean?

It means, when started, all web servers looked for an application (Ruby class or object) that satisfied the following three conditions:

  1. It has a call method.
  2. It accepts the env object representing the HTTP request (don't worry, it's just a Ruby Hash).
  3. It returns an array containing three values: the status code, the headers, and the response.
class App
   def call(env)
     headers = { 'Content-Type' => 'text/html' }
     
     response = ['<h1>Greetings from Rack!!</h1>']
     
     [200, headers, response]
   end
end

This is what everyone means when they say “Rack provides a minimal, modular, and adaptable interface.” You can use any class or object that satisfies the above three conditions with any web server, and everything will still work as expected.

Every rack compliant webserver will always invoke a call method on an object (the Rack application) and serve the result of that method.

If you're curious how it works in Rails, here's the config.ru file in your Rails app:

# This file is used by Rack-based servers to start the application.

require_relative "config/environment"

run Rails.application
Rails.application.load_server

The Rails.application has a call method defined in the Rails::Engine class, the superclass of Rails::Application.

module Rails
  class Engine < Railtie
  
    # Define the Rack API for this engine.
    def call(env)
      req = build_request env
      app.call req.env
    end
  end
end

In a future post, we'll learn how Rails handles incoming requests (it's fascinating), but let's continue learning Rack for now.

So far, we've seen how the same Ruby application works with multiple servers. However, the other way works, too. You can replace our simple application with another script, a Rails application, or even a Sinatra application, and any Rack-compliant web server can run it without a problem.

Rack allows application frameworks & web servers to communicate with each other, and replace each without changing the other.

This is Rack’s main benefit.

Rack provides a common protocol (or interface, or specification) that different web servers can use to talk to different web applications, without worrying about the internals of each.

The Problem: Before Rack came along, each framework had to understand each other web server’s API to communicate with it.

The Solution: With Rack, the creators of web servers and web frameworks all agreed to talk to each other using a standardized way to reduce the efforts involved with communicating with different standards.

If you are building the next high-performant web server or application framework, you can talk with any other rack-compliant web application or web server as long as you follow the Rack specification.

This is pretty useful. Now let’s learn some theory.