How does Rack work?
Sometimes, developers who use Rails don’t understand Rack, what it does, or what it’s for.
To explain this, let’s go back to early 2007. At that time, Rails was at version 1.2, Merb was taking its first steps far from merging with Rails, and other frameworks like Camping, Nitro, Ramaze, and Iowa also existed. Just like today, we need an app server to run our applications. We had options such as WEBrick, Mongrel, and FastCGI back then.
The problem was that each app server had its own interface, startup process, and parameters. This meant frameworks had to implement specific handlers for each of the most popular app servers and allow developers to create adapters for unsupported servers. The result? A lot of code duplication across existing frameworks and those yet to come.
For example, we have mongrel_rails
, the handlers for FastCGI
and WEBrick
from Rails, just as Camping also has for FastCGI
and WEBrick
.
That’s when Rack emerged—a simple API inspired by Python’s WSGI. It enabled communication between frameworks and servers without requiring the handlers previously required in each framework. After its adoption, frameworks no longer needed to announce integration with specific servers; they just had to say they were Rack-compatible. On the server side, it was also straightforward: they only needed to add a Rack handler, making server swapping a plug-and-play experience.
So, how do you create a Rack-compatible application?
With a lambda, you can already have an app up and running. Check out a simple “Hello World” example below. Create a config.ru
file with the following code:
my_app = ->(env) { [200, {}, ["Hello World!"]] }
run my_app
run gem install rackup
to install the rackup
gem (previously part of the Rack gem), run the rackup
command, and your Rack app will be running on default port 9292:
~ curl localhost:9292
Hello World!
What makes up a Rack app?
Any object that responds to the #call
method, accepting an env
argument (a hash representing the HTTP request) and returning an array with three elements, can be considered a Rack app.
These elements are:
- The HTTP status of the response;
- A Hash with HTTP headers;
- An object that responds to
#each
or#call
as the response body.
Middlewares
Another Rack feature is the use of middlewares, which act as filters for the request or response. Middlewares are themselves Rack apps, following the same API. The Rack gem allows them to be stacked in layers. Here’s an example of a middleware:
class MyMiddleware
def initialize(app)
@app = app
end
def call(env)
unless env["HTTP_ACCEPT"].include? "text/html"
return [401, {"content-type" => "text/plain"}, ["Not Authorized!"]]
end
@app.call(env)
end
end
To use it, modify the config.ru
file from the previous example:
require 'my_middleware'
my_app = -> (env) { [200, {}, ["Hello World!"]] }
use MyMiddleware
run my_app
In this case, the middleware checks if the Accept header contains text/html
. If not, it returns a 401 error and halts processing; otherwise, it passes the request along with @app.call(env)
.
$ curl -i localhost:9292 -H Accept:text/html
HTTP/1.1 200 OK
Content-Length: 12
Hello World!
$ curl -i localhost:9292
HTTP/1.1 401 Unauthorized
content-type: text/plain
Content-Length: 15
Not Authorized!
Rack emerged as a solution to a common problem in the early 2000s: the lack of standardization between frameworks and servers in Ruby. By offering a simple API, it eliminated code duplication and made web application development more modular and less coupled. Rack remains a cornerstone of the Ruby ecosystem, supporting frameworks like Rails and simplifying developers lives to this day.