Rack: Part II
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.
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
endRack::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
endclass 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
endRack::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
endclass 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
endclass 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
endRack::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
endclass 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
endclass 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 FriendlyGreetingrun 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 anHTTP
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!