Rack Deep Dive – Part 1
About 7 months ago, I started teaching ruby and rails at Andela. One of the biggest challenge in teaching rails is explaining all of the “magic” that Rails uses to do its job . The most effective way to really understand how things work in rails is to rebuild it from scratch. Below is a quote by Chad Fowler.
The magic thing about Rails is when I looked at it the first time, I knew how it worked because I already knew Ruby really well.
I saw all the metaprogramming tricks; they were almost transparent, looking at it. But I didn’t know you could put it together that way and create something so expressive and succinct.
This is my attempt at building an MVC framework similar to rails to teach my fellows how rails work?
I broke down this series into multiple posts which I will be posting gradually. The posts are
-
Part 1 – Rack Deep Dive
-
Part 2 – Set up a Basic Framework
-
Part 3 – Autoloading and Utility Methods
-
Part 4 – Better Routing
-
Part 5 – Render, Redirect & Before_Action Methods in Controllers
-
Part 6 – Extract Complexities out of View with Helpers
-
Part 7 – Reduce Rework with Generators
-
Part 8 – Set up ORM for Read/Write to Database
-
Part 9 – Generate Database from Schema
-
Part 10 – Set up Migrations
For this series of posts, I’m going to assume that you have a general understanding of HTTP (for example: 2xx status code means success) and Ruby, though you may never have built a web framework before.
To understand Rails and even the more basic Sinatra, I think you need to go deeper and start with Rack.
What is Rack
Did you know that rails and sinatra are rack apps, so also is our future MVC framework. So what exactly is rack and how does it work.
In a sentence, Rack provides a minimal interface between webservers that support Ruby and Ruby frameworks. It is a Ruby package that provides an easy-to-use interface to the Ruby Net::HTTP library.
All major Ruby web servers (Puma, WEBrick, Unicorn, etc) understand the Rack protocol, so if our app conforms to the Rack application specification, we can use those servers with it for free.
Since our MVC framework will be a rack app, understanding rack and it’s specification is necessary before we can proceed towards building our framework.
Rack Specification
From rack website(http://rack.github.io)
To use Rack, provide an “app”: an object that responds to the call method, taking the environment hash as a parameter, and returning an Array with three elements:
- The HTTP response code
- A Hash of headers
- The response body, which must respond to each
You basically provide an object(not a class) that responds to call, passing in another object when the call method is called which will return another object. It is all ruby.
To fully understand rack and it’s specification, let’s build a rack app.
A Tiny Rack App
We are going to build a rack app with 3 lines of code. Create a file called tiny_rack_app.rb and add the following content to it.
1 2 3 4 5 |
require "rack" app = Proc.new { |env| [200, {}, ["I respond to all request"]] } Rack::Handler::WEBrick.run app, Port: 9292 |
In the first line, we required rack. In the second line, we created an object that responds to call. Remember that a proc has a call method and it executes the block passed to it during initialization. Notice also that the block passed to the proc accepts env
as an argument and returns a rack compatible response(from specification).
The third line is used to boot our ruby server. You will notice this line:
1 |
Rack::Handler::WEBrick |
Rack uses handlers to run Rack applications. Each Ruby webserver has its own handler, but I chose the WEBrick handler for this example, because WEBrick is installed by default with Ruby. You can use other handlers like Thin which is installed by default or you can use web servers like Puma
and Unicorn
by installing and requiring the necessary gems. Run it using:
1 |
$ ruby tiny_rack_app.rb |
and navigate to localhost:9292. You will see I respond to all request
in your browser.
We can also use the rackup command line tool(with config.ru file) and avoid specifying details like port and server until runtime.
1 |
run ->(env) { [200, {}, ["I respond to all #{env["REQUEST_METHOD"]} request"]] } |
In this case, I am using a lambda instead of a Proc. Run it using
1 |
$ rackup --port 9292 |
and navigate to localhost:9292. You will see I respond to all GET request
in your browser.
Another Rack App
Here is a rack app that is created from a custom class that responds to call method and that uses puma handler. Remember to install puma gem using gem install puma
before using this handler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
require "rack/handler/puma" class AnotherRackApp def call(env) http_verb = env["REQUEST_METHOD"] path = env["PATH_INFO"] status = 200 headers = {"Content-Type" => "text/html"} body = ["got #{http_verb} <b>request</b> from <strong>#{path}</strong> path"] [status, headers, body] end end app = AnotherRackApp.new Rack::Handler::Puma.run app, Port: 9292 |
Paste the above code in a file called another_rack_app.rb
and run it using
1 |
$ ruby another_rack_app.rb |
Some interesting things are happening in the call method. It accept’s an environment hash as an argument(like any other rack app), then gets the verb and path from the env hash. In line 8
, we set the response content-type
header to be text/html
and constructed a html response body in line 9
. The final content returned in line 10
is a rack compatible response.
The final content returned in line 10
is a rack compatible response.
Environment Hash
Our Rack server object takes in an environment hash. What’s contained in that hash? Here are a few of the more interesting parts:
REQUEST_METHOD
: The HTTP verb of the request. This is required.PATH_INFO
: The request URL path, relative to the root of the application.QUERY_STRING
: Anything that followed?
in the request URL string.SERVER_NAME
andSERVER_PORT
: The server’s address and port.rack.version
: The rack version in use.rack.url_scheme
: is ithttp
orhttps
?rack.input
: an IO-like object that contains the raw HTTP POST data.rack.errors
: an object that response toputs
,write
, andflush
.rack.session
: A key value store for storing request session data.rack.logger
: An object that can log interfaces. It should implementinfo
,debug
,warn
,error
, andfatal
methods.
You can wrap the environment hash in a Rack::Request
object to work with the environment hash in a more object oriented way. eg.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def call(env) request = Rack::Request.new(env) # env["REQUEST_METHOD"] verb = request.request_method #=> GET, POST, PUT, etc. # env["REQUEST_METHOD"] == "POST" request.post? # env["PATH_INFO"] path = request.path_info # env["rack.session"] request.session # access to the session object, if using the # Rack::Session middleware # env["rack.request.query_hash"] + env["rack.input"] request.params # a hash of merged GET and POST params, useful for # pulling values out of a query string # ... and many more end |
Accessing env
directly quickly becomes tedious in any Rack project, more complex than AnotherRackApp. You should use Rack::Request
Middleware
Middleware gives you a way to compose Rack applications together.
In the real world, your rack app won’t work in isolation. More often you want to process the request or response before it hits your final rack app. Middleware
Middlewares are other Rack applications that comes between our final app and the HTTP request. Middleware is Rack’s true strength.
For example, if you have a Rails app lying around (chances are, if you’re a Ruby developer, that you do), you can cd
into the app and run the command rake middleware
to see what middleware Rails is using:
1 2 |
$ cd my-rails-app $ rake middleware |
Below is an example middleware.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
require "rack" class MethodOverrideMiddleware def initialize(app) @app = app end def call(env) request = Rack::Request.new(env) if request.params["method"] env["REQUEST_METHOD"] = request.params["method"].upcase end @app.call(env) end end |
The difference between a middleware and your final rack app is that it’s initialized with an app
, which is the “final” Rack app that it can pass the request on to.
The middleware above updates our request method(verb) with the value of method
query param if it is set. So we can we can make a get request and change it to a post request like this: localhost:9292?method=post
.
You can combine your middleware with your rack app using Rack::Builder
.
For example, you can combine the MethodOverrideMiddleware
middleware with the AnotherRackApp
rack app above using
1 2 3 4 5 6 |
app = Rack::Builder.new do use MethodOverrideMiddleware run AnotherRackApp.new end Rack::Handler::WEBrick.run app, Port: 9292 |
Try putting the middleware code and the builder code in another_rack_app.rb
file and remove the following lines.
1 2 |
app = AnotherRackApp.new Rack::Handler::Puma.run app, Port: 9292 |
Run it with
1 |
$ ruby another_rack_app.rb |
You are not limited to one middleware. You can stack as many middleware as possible as seen in rails.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
use ActionDispatch::Static use Rack::Lock use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007f8e03fbb560> use Rack::Runtime use ActionDispatch::RequestId use Rails::Rack::Logger use ActionDispatch::ShowExceptions use ActionDispatch::DebugExceptions use ActionDispatch::RemoteIp use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending use ActiveRecord::ConnectionAdapters::ConnectionManagement use ActiveRecord::QueryCache use ActionDispatch::ParamsParser use Rack::Head use Rack::ConditionalGet use Rack::ETag run TodoList::Application.routes |
With this, we are done with the discussion on rack.
In the next post, we will start building out the MVC Framework.
If you have any questions or observations, please drop your thoughts in the comment section below

- Ruby on Steroids(DSLs): The Powerful Spell Called DSL - March 14, 2016
- Ruby on Steroids: The Magic of MetaProgramming – Method Spells - March 12, 2016
- Ruby on Steroids: The Magic of MetaProgramming – Fellowship of Spells - February 28, 2016
- Ruby on Steroids: The Magic of MetaProgramming – An Unexpected Journey - February 27, 2016
- How to Delegate Like a Boss - February 24, 2016
- Better Routing – Part 4 - January 28, 2016
- Autoloading and Utility Methods – Part 3 - January 11, 2016
- Set up a Basic MVC Framework – Part 2 - December 22, 2015
- Rack Deep Dive – Part 1 - December 17, 2015