Rack: Part II

Austin Miller
7 min readOct 29, 2020

This is the second in a series of two articles that discuss the Rack RubyGem — what it is, where it sits in server-side development, what it means for an application to be Rack-based, and what Rack accomplishes for developers behind the scenes.

This article builds on the fundamental concepts of Rack discussed in the first article and focuses on the concept of Rack middleware, what qualifies a Ruby application to be a Rack middleware, and what middlewares can do for your application. We’ll implement two Rack middleware applications for my_app.rb that we worked on earlier and tell Rack to use our middleware stack when it runs our web application.

You can find the first article here.

What is Rack middleware?

Rack is an interface between an application and an application server. It abstracts away the specifics of handling HTTP requests with a call method, and even works with other servers to handle necessary socket programming to start a server. But it does even more.

Rack allows you to chain Rack-based classes together. You can write your own Rack-based applications to chain to your primary application, or you can use some of the many middleware applications that Rack includes in its library.

Practically speaking, Rack middleware allows developers to build in many functionalities to their applications, from logging to basic authentication to stopping spam — before they even start working on their application!

How does middleware work?

As we touched on earlier, Rack-based applications must define a call method. The call method must take one argument and return a 3-item Array with what will be used to form a response status, response headers, and response body.

Classes that satisfy these requirements can be chained together. This implies that we can chain as many so-called middleware applications to our app as we want. Many different small, modular applications can be chained together to create a middleware stack of technologies.

Photo by Julia Kadel on Unsplash

Let’s work with the simple Rack-based application we set up earlier and add a FriendlyGreeting middleware class to give it some (minimal) functionality. Here’s our simple MyApp class.

class MyApp
def call(env)
['200', { "Content-Type" => "text/plain" }, ["hello world"]]
end
end
Rack::Handler::WEBrick.run MyApp.new

To give our Rack-based application some more functionality, without changing anything in the MyApp class itself, let’s define a new class. Let’s say we want to display a friendlier greeting in the browser.

class FriendlyGreeting
def initialize(app)
@app = app
end
end

To start, middleware applications must be defined with an initialize method. The initialize method must take one argument. When ::new is invoked on FriendlyGreeting, an instance of the application one level down the chain will be passed in. The status code, response headers, and response body will then be accessible in our new middleware class. In our simple example, we’d just like to access the response body and prepend it with a friendlier greeting.

Our new class must also adhere to Rack specifications, so let’s write our FriendlyGreeting#call method with these requirements in mind.

class FriendlyGreeting
def initialize(app)
@app = app
end
def call(env)
body = @app.call(env).last
[
'200',
{ "Content-Type" => "text/plain" },
body.prepend("A warm welcome to you!\n")
]
end
end

In our call implementation, we actually invoke MyApp#call and pass in env. We then invoke Array#last to access the third element of the return value of MyApp#call, which will be our response body. Finally, we just prepend it with another string and a newline.

The final piece of the puzzle is to tell Rack how to run our chained applications. We can invoke the same Rack::Handler::WEBrick.run method, but what should we pass in as our application?

If we think about our FriendlyGreeting class, we define our initialize method to take an argument, which should be an app. This points us in the right direction. We should pass in an instance of the FriendlyGreeting class, which should take an instance of MyApp as an argument.

class MyApp
def call(env)
['200', { "Content-Type" => "text/plain" }, ["hello world"]]
end
end
class FriendlyGreeting
def initialize(app)
@app = app
end
def call(env)
body = @app.call(env).last
[
'200',
{ "Content-Type" => "text/plain" },
body.prepend("A warm welcome to you!\n")
]
end
end
Rack::Handler::WEBrick.run FriendlyGreeting.new(MyApp.new)

When we run ruby myapp.rb in the command line and visit the browser, we should see our more friendly greeting precede our original greeting.

Rack Builder

This is great. Rack abstracts away sockets and servers, HTTP requests and responses, and even gives us the opportunity to build in modular, reusable chunks of code to accomplish common development tasks.

One last topic we’ll briefly touch on before we wrap up this discussion is Rack::Builder. Rack::Builder is a DSL that gives us the use method to more concisely incorporate middlewares with our web application. Let’s say we wanted to add one more middleware to our application, perhaps a Wave class that displays one more greeting before our other greetings in the browser, without touching either of our two existing classes.

Let’s implement a Wave class, define an initialize method that will take one argument and, assign it to @app , and define a call method that will take one argument for the Hash of information about the request. Finally, let’s tell Rack to run our three-level application.

class MyApp
def call(env)
['200', { "Content-Type" => "text/plain" }, ["hello world"]]
end
end
class FriendlyGreeting
def initialize(app)
@app = app
end
def call(env)
body = @app.call(env).last
[
'200',
{ "Content-Type" => "text/plain" },
body.prepend("A warm welcome to you!\n")
]
end
end
class Wave
def initialize(app)
@app = app
end
def call(env)
body = @app.call(env).last

[
'200',
{ "Content-Type" => "text/plain" },
body.prepend("Wave from afar!\n")
]
end
end
Rack::Handler::WEBrick.run Wave.new(FriendlyGreeting.new(MyApp.new))

This works, but you can see that it’s starting to get a little messy. What if your application used 10 different middlewares?

That’s where Rack::Builder comes in. Rather than nesting instances of classes together to pass to ::run, we can simply invoke Rack::Builder::use to accomplish this is a cleaner syntax. To implement this in the simplest way, we’ll create a config.ru rackup file in the same directory as our myapp.rb file and require our myapp.rb file there. We’ll have one file with our three classes and one file for our rack configuration.

#myapp.rb
#require 'rack'
class MyApp
def call(env)
['200', { "Content-Type" => "text/plain" }, ["hello world"]]
end
end
class FriendlyGreeting
def initialize(app)
@app = app
end
def call(env)
body = @app.call(env).last
[
'200',
{ "Content-Type" => "text/plain" },
body.prepend("A warm welcome to you!\n")
]
end
end
class Wave
def initialize(app)
@app = app
end
def call(env)
body = @app.call(env).last
[
'200',
{ "Content-Type" => "text/plain" },
body.prepend("Wave from afar!\n")
]
end
end

Note that if we run our config.ru file to start a server and run our application, we don’t need to require 'rack' in our main .rb file. Running .ru files using the rackup command automatically requires Rack. We’re no longer invoking any Rack methods in our main .rb file. That logic has been separated into our config.ru file.

#config.ru
require_relative
"myapp"
use Wave
use FriendlyGreeting
run MyApp.new

The .ru file extension will allow us to invoke Rack’s use method and run method without explicitly prefixing them with classes and modules. We will also run the program in the command line using rackup config.ru, which is a command provided by the Rack gem available to run .ru files.

We can list out as many enclosing middlewares as we need by invoking use. This will also preserve the order in which middlewares are used — the last-listed middleware uses the return value of the main application, the second-to-last middleware uses the return value of the last middleware, etc., etc..

Rack middlewares make Rack a powerful part of your web application. You can incorporate as many or as few modular classes you need for your app, building as much or as little logic into your program as you’d like.

Conclusion

In these articles, we’ve talked about Rack. Rack is the 9th-most downloaded gem and a powerful interface.

Rack …

  • Abstracts low-level tasks like parsing information from HTTP requests and formatting the return value of your application back into an HTTP response that the server can understand. Applications can then be fully focused on business logic .
  • Generalizes application-to-server communication. Rack-based apps — whether they’re written using Ruby, Sinatra, Rails, or another framework — can establish socket connections using any Rack-supported servers, whether that’s WEBrick, Puma, Passenger, etc..
  • Provides an architecture for using modular pieces of functionality. Rack middlewares give the web developer a low-level opportunity to implement all kinds of functionalities by creating a middleware stack of chained Rack-based applications as part of the their application.

At its core, Rack allows your Ruby application to focus on generating dynamic content and making server-side changes — it takes care of lower-level tasks so you don’t have to think about them.

This article came from a desire to better understand Sinatra’s source code, and it has led me down a rabbit hole from which I’m excited to emerge. I am a student at Launch School, and much of the higher level content has come from coursework and student content. Below are the numerous additional materials you might be interested in reading.

A big thank you to Launch School — the staff who make it all possible and the students who are such a pleasure to work with.

Thank you so much for reading along. Study on!

--

--

Austin Miller

Research analyst turned aspiring web developer. Learning the fundamentals with Launch School. Lives in Denver, CO.