Rack, The Underbelly Of Every Great Ruby Web App
Rack is used for practically every framework that runs on Ruby yet many Rails developers don't really understand what it is or how it works. Understanding rack can make these frameworks feel less daunting. It's surprisingly easy to understand when you take a look at it.

Rack's interface and principals are actually very simple, it's just that we typically never need to think about them when working with Rails, Sinatra, Hanami or other frameworks built on top of Rack.
So what does Rack actually do?
It is an interface between your web server and your application code! Rack listens to the web server through a handler and then calls your application with one argument, the Rack Environment. To comply with the interface your application must return an array with 3 items, the status, headers, and the body. The status must be an integer which matches a valid HTTP status code. The headers should be a hash of header key value pairs. Finally the body must be an object that responds to each, typically an array of strings.
Hello, World?
Let's build our first Rack app. Rack has built in handlers for various web servers so we'll use one of those.
# hello_world.rb
require 'rack'
class HelloWorld
def call(env)
[200, {"Content-Type" => "text/plain"}, ["Hello, World!"]]
end
end
Rack::Handler::WEBrick.run HelloWorld.new
Now start it up and visit localhost:8080
ruby hello_world.rb
Let's Rackilize Our Hello, World! App.
The application above is called through the ruby executable, however Rack provides an executable named rackup to start our rack applications for us. This executable expects a file at the root of your application called config.ru (r[ack]u[p]) which is used to configure the rack middleware stack.
require 'thin'
class HelloWorld
def call(env)
[200, {"Content-Type" => "text/plain"}, ["Hello, World!\n"]]
end
end
class GoodbyeWorld
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
body << "Goodbye, World!\n"
[status, headers, body]
end
end
use GoodbyeWorld
run HelloWorld.new
Now we've converted our Rack script to a Rack App configuration. We've also made a couple of changes. We've required thin web server which will automatically switch the handler out. We've also added our first middleware to the stack, GoodbyeWorld. And finally we can use the rack DSL methods, use and run. So what does this middleware do? In this case we're running the app first and then we're adding our "Goodbye, World!" to the response body. We can do other, not-so-contrived, things with middleware too. Let's add a simple profiler.
require 'thin'
class HelloWorld
def call(env)
[200, {"Content-Type" => "text/plain"}, ["Hello, World!\n"]]
end
end
class GoodbyeWorld
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
body << "Goodbye, World!\n"
[status, headers, body]
end
end
class Benchmarker
def initialize(app)
@app = app
end
def call(env)
start_time = current_time
status, headers, body = @app.call(env)
end_time = current_time
body << "Took #{end_time - start_time} seconds to complete"
[status, headers, body]
end
def current_time
Time.now.to_f * 1000
end
end
use Benchmarker
use GoodbyeWorld
run HelloWorld.new
Modifying the Environment
So far we've only shown how to wrap the application, let's change that so we're just modifying environment instead. It's pretty simple, rather than calling the app and modifying the response, we modify the env and then call the app.
require 'thin'
class HelloWorld
def call(env)
if env['flip'] == 'heads'
[200, {"Content-Type" => "text/plain"}, ["Hello, World!\n"]]
else
[401, {"Content-Type" => "text/plain"}, ["Naughty naughty\n"]]
end
end
end
class CoinFlip
def initialize(app)
@app = app
end
def call(env)
env['flip'] = %w[heads tails].sample
@app.call(env)
end
end
use CoinFlip
run HelloWorld.new
Halting execution
If any of the middlewares return a response the rest of the stack will not be processed. This is used by some rack middlewares like rack-attack which prevent attackers from making malicious requests to our app. Let's modify the logic make our requests win randomly.
require 'thin'
class HelloWorld
def call(env)
[200, {"Content-Type" => "text/plain"}, ["Hello, World!\n"]]
end
end
class CoinFlip
def initialize(app)
@app = app
end
def call(env)
if %w[heads tails].sample == 'heads'
@app.call(env)
else
[200, {"Content-Type" => "text/plain"}, ["Winner winner chicken dinner!\n"]]
end
end
end
class GoodbyeWorld
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
body << "Goodbye, World!\n"
[status, headers, body]
end
end
use CoinFlip
use GoodbyeWorld
run HelloWorld.new


