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:
- Making Ruby Talk to Web Servers with Rack
- The Rack Protocol
- The Rack Gem
- Middleware Toolbox
- Tools to Build Rack Apps and Middleware
- The
rackup
Command - The Rack DSL
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:
- How did all of these web servers know how to run our application?
- 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:
- It has a
call
method. - It accepts the
env
object representing the HTTP request (don't worry, it's just a Ruby Hash). - 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.