The Life‑Changing Magic of Ruby and Rails

The Definitive Guide to Rack for Ruby and Rails Developers

In this article, I will try to explain pretty much everything that you need to know about Rack as a Ruby and Rails developer. It covers everything from the basics to more advanced stuff such as middleware and writing your custom middleware.

Rac
Rack

You’ve been around in the Rails world for a while. You know your way around rails. But you keep hearing this 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. It covers everything from the basics to more advanced stuff such as middleware and writing your custom middleware. It’s quite a long article, but if you stick till the end, you will know more about Rack than pretty much everyone else.

But first, before we try to understand the theory behind Rack, let’s try to use it in a real application.

Building a Web Application without Rails

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).

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 web server is the 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 should see something like this:

Stop the server using Ctrl + c command.

Now let’s run our 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.

A few questions you might have at this point:

  1. How did all of these web server 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 they all are rack-compliant web servers.

Okay, but what does that mean?

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

  1. It has a call method.
  2. It accepts the env object representing the HTTP request.
  3. It returns an array containing three values: the status code, the headers, and the response.

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.

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

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. It 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.

Before Rack came along, each framework had to understand each other web server’s API to communicate with it. 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.

The Rack Specification

Here’s the standard definition of Rack that you have heard.

Rack provides a minimal, modular, and adaptable interface for developing web applications in Ruby.

At the very basic level, Rack is a protocol, just like HTTP. Rack provides a layer between the framework (Rails) & the web server (Puma), allowing them to communicate with each other.

The Rack protocol allows you to wrap HTTP requests and responses in simple interfaces. This simplifies the API for web servers and frameworks into a single method call.

This is exactly similar to how your browser talks to the web server using the HTTP protocol. That allows any client (Chrome, Firefox, or even terminal commands) to talk to any backend server written in any language (PHP, .NET, Ruby, etc.) so you can access the website.

When we say an application is Rack-compliant, it means three things:

  1. It has a call method
  2. The call method accepts a single argument env, containing all the data about the request.
  3. The call method returns an array containing the following:
  • The status code, e.g. 200 for success
  • A hash containing the headers, and
  • An array containing a single string, which is the response body.

The Rack Gem

So far, we’ve only talked about the Rack specification. But there’s also a gem called Rack. You can find the source code for it on Github, read the documentation on Rubydoc, and install it like any other Ruby gem.

gem install rack

Why do we need the Rack gem if the rack-compliant web servers and frameworks can communicate without it? Here are a few big reasons:

1. Middleware Toolbox

Because the Rack interface is so simple, you can use any code that implements this interface in a Rack application. This allows you to build small, focused, and reusable applications which work together to provide different functionalities. These mini-components are known as Middleware.

Middleware sits between the user and the application code. When an HTTP request comes in, the middleware can intercept, examine, and modify it. Similarly, it can examine and modify the HTTP response before forwarding it to the user.

Middleware is very useful for writing logic that is not specific to your web application, such as authenticating the request, logging, or exception handling. It focuses on doing one thing and doing it well. Using middleware also simplifies your application code, and it can only focus on the logic related to the application.

For example, here’s a middleware that logs to the console

  1. before it passes the request to the next application in the pipeline and
  2. after it receives the response on its way out.
class Logger
  def initialize(app)
    @app = app
  end

  def call(env)
    puts "Received the incoming request"
   
    # forward the request to the next middleware or app 
    status, headers, body = @app.call(env)
   
    puts "Received the outgoing response"

    [status, headers, body]
  end
end
  1. This middleware accepts the app in the constructor. The app can be another middleware in the pipeline or the actual application.
  2. When called, it first deals with the incoming request represented by the env object. Here, we print “Received the incoming request”.
  3. After handling the request, it passes the request to the next middleware (or app) in the pipeline,
  4. Upon receiving the response (status, headers, body) from the next middleware (@app), it logs to the console.
  5. Finally, it passes the response to the next middleware in the pipeline. This can be the middleware that called our logging middleware, or the web server.

Here’s a diagram that will help you visualize the middleware pipeline.

The Rack gem provides many such components that you can use. All these components use the same Rack interface.

Here are some middleware components included in the Rack gem.

  • Rack::Files, for serving static files.
  • Rack::Config, for modifying the environment before processing the request.
  • Rack::ContentLength, for setting content-length header based on body size.
  • Rack::ContentType, for setting default content-type header for responses.
  • Rack::Deflater, for compressing responses with gzip.
  • Rack::Head, for returning an empty body for HEAD requests.
  • Rack::Logger, for setting a logger to handle logging errors.
  • Rack::Reloader, for reloading files if they have been modified.
  • Rack::ShowException, for catching unhandled exceptions and presenting them in a nice and helpful way with clickable ‘backtrace.
  • Rack::ShowStatus, for using nice error pages for empty client error responses.

You can find a complete list here. Pick and choose whatever you like, in any way you wish.

2. Tools to Build Rack Applications and Middleware

The Rack gem provides the infrastructure code that you can use to build your own web application, web framework, or middleware. It allows to quickly prototype or build stuff without doing the same repetitive tasks again and again.

Here are some helpers you can use with Rack:

  • Rack::Request, which also provides query string parsing and multipart handling.
  • Rack::Response, for convenient generation of HTTP replies and cookie handling.
  • Rack::MockRequest and Rack::MockResponse for efficient and quick testing of Rack application without real HTTP round-trips.
  • Rack::Cascade, for trying additional Rack applications if an application returns a not found (404) or method not supported (405) response.
  • Rack::Directory, for serving files under a given directory, with directory indexes.
  • Rack::MediaType, for parsing content-type headers.
  • Rack::Mime, for determining content-type based on file extension.

Since the Rack interface is so simple, anyone can build and publish useful middleware, and the community did build many such components. The rack-contrib project on Github contains a library of these middleware components. Take a look; you will definitely find something useful.

3. The rackup command

The Rack gem ships with the rackup command. It is a useful tool for running Rack applications with a web server. It uses the Rack::Builder DSL (domain-specific language) to configure middleware and easily compose applications.

rackup automatically figures out the environment it is run in, and runs your application as WEBrick, Puma, or any web server—all from the same configuration. Let’s try that now.

So far, we’ve been launching our web application using a web server-specific command, i.e. puma, thin start, or unicorn. Using rackup lets us be web server agnostic. In the same directory containing your config.ru file, run rackup command:

➜ rackup

Puma starting in single mode...
* Puma version: 5.6.4 (ruby 2.7.1-p83) ("Birdie's Version")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 28318
* Listening on http://127.0.0.1:9292
* Listening on http://[::1]:9292
Use Ctrl-C to stop

Since I have Puma installed, it launched the application in Puma. If you have some other web server, rackup will use that.

4. Convenient DSL

If you aren’t familiar with a DSL, it stands for Domain Specific Language. It is a programming language with a higher level of abstraction optimized for a specific class of problems. A DSL uses the concepts and rules from the field or domain. For example, Ruby on Rails provides a DSL for building web applications.

Rack provides such a DSL consisting of the following methods:

  • run: It takes a Ruby object responding to method call as an argument and invokes the call method on it. We used this method in our example at the beginning of this post.
  • map: It takes a string and a block as parameters and maps incoming requests against the string. If it matches, Rack will run the block to handle that request. This is similar to how routing works in Rails.
  • use: It includes the middleware to the rack application.

Additionally, the Rack::Builder class helps you with iteratively composing Rack applications.

Let’s use all these tools to build another web application. First, replace the code in the config.ru file with the following:

# config.ru

require_relative './app'

app = Rack::Builder.new do
  use Rack::ShowExceptions
  use Rack::Logger
  
  map "/welcome" do
    use Rack::Lint
    run App.new
  end
end

run app

This DSL tells Rack to use the Rack::ShowExceptions and Rack::Logger middleware before using our App.

Finally, add a new file app.rb in the same directory with the following code:

# app.rb

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

Now, run the rackup command, and navigate to /welcome page on the server. You should be greeted with the response from our application.

When a request comes from the web server, Rack will first use the exception-handling middleware, then the logging middleware, and finally run the app. The app will process the incoming request, and return the response, which will again be examined and processed by the middleware stack, this time in the reverse sequence.


To summarize what we’ve learned so far, the Rack protocol powers the web in the Ruby world. It consists of two things:

  1. The Rack specification provides a simple interface that allows web servers and applications to talk to each other.
  2. The Rack gem provides tools that let us iteratively compose our Rack-compliant applications.

Additionally, Rack middleware components allow you to separate the application logic from peripheral concerns like authentication, logging, and error-handling. You can use any middleware built by the community for everyday use cases.

That’s it. I hope that now you have a much better understanding of Rack and the surrounding ecosystem.

Subscribe to Akshay's Blog

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe