In Progress
Unit 1, Lesson 21
In Progress

Rack

Do you really need that web framework?

Sitting underneath Ruby on Rails, Sinatra, Hanami, and just about every other Ruby web framework is the simple web application framework known as Rack. In this episode you’ll learn how to write a basic web application in pure Rack. You might be surprised at how easy it is!

Video transcript & code

/You may remember today’s guest chef from Episode #447, when he introduced the Pessimize gem. That’s right, RubyTapas station chef Federico Iachetti is back, with another episode on one of his favorite topics: small, sharp tools. This time around, the tool in question is Rack. Enjoy!/

— Avdi

We have a new awesome product that's been on development for a long time. We're very close to having a beta version for users to try out. We decide that before launching, we want to grab the email addresses of potential clients in order to do a pre-sale.

In order to do that, we go our usual route. We open a terminal and we cast the magic spell.

rails new email_catcher

And now, everything is solved for us. We just need to create a model, view and controller, set the database, And start the server.

rails g scaffold contact email
rails db:create
rails db:migrate
rails s

And now, we just send our potential contacts to our server (http://localhost:3000/contacts/new)

That's the beauty of Rails. We can have an application up and running Just a couple commands away.

But at what cost?

Lets take a look at some numbers.

Lets first run the loc_counter gem to count how many lines of code our new app has.

We see that, for a very basic web app that just captures emails, we've created almost 320 lines of executable code. Which we are using probably less than 1% of.

tapas@tapas-box /tapas/email_catcher $ cd email_catcher
tapas@tapas-box /tapas/email_catcher $ loc_counter .
37 files processed
Total     682 lines
Empty     118 lines
Comments  244 lines
Code      320 lines
tapas@tapas-box /tapas/email_catcher $

We next run bundle again and we pipe it through the word counter utility that Linux provides.

We see 64 lines. Which means that we have 62 gem dependencies (2 of those lines are just information)

tapas@tapas-box /tapas/email_catcher $ bundle | wc
     64     207    1577

Counting the more than 21.000 lines of Rails code plus the thousands of lines of code added by the dependencies, that's a lot of code! Is all this weight justified by a one-field form? More pertinently, could we achieve the same result without all that baggage?

You might be thinking, maybe we could use a lightweight framework, like Sinatra. But what if we don't need to use a framework at all?

At the foundation of every modern Ruby web framework is a library called Rack. What would it look like if we just wrote this app on bare rack, and nothing else? Is it possible? Is it advisable? Let's find out!

We first need to require Rack. Then we need an app.

The app is just an object that satisfies three requirements:

  • It responds to the call message. A Proc will fulfill this one.
  • It receives an environment object.
  • It returns a three element Array

The first element corresponds to the status code. For this trivial example, we just return a 200 status.

The next element contains a hash, the response headers. We'll just add one header indicating that the response is an HTML string. This will tell the browser to format it as HTML.

The last element is the HTML body.

Finally, we need to call a handler to run our application.

We're using the WEBrick handler here. WEBrick is a pure-Ruby web application server that comes bundled with Ruby, so it's a good default choice.

require "rack"

app = Proc.new { |env|
  [
    200,
    {'Content-Type' => 'text/html'},
    ["<h1>Hello, world</h1>"]
  ]
}

Rack::Handler::WEBrick.run app

Now we can run it by invoking it on the command line

ruby config.ru

And we can browse to http://localhost:8080/ to find our HTML rendered.

In this particular implementation, we've hardcoded Webrick as the web server right in the app. We leave this choice open to configuration by just calling the run method, provided by Rack.

run app

After this change, we now need to invoke our app using the rackup command. Notice that the file needs to have the .ru extension. The default convention for configuration files is to name them config.ru.

rackup config.ru

We also notice that the server port has changed to 9292. So we browse to http://localhost:9292/ and we still get our rendered body.

Having the file named config.ru also allows us to simply ignore the file name. We'll add a call to rerun so we don't have to restart rack every time we make a change. If you want to know more about the rerun gem, watch Episode #320.

rerun rackup

Lets start writing our real app. We want to render a form. It'll point to the root path of our app. And it'll have a text input called name.

require "rack"
require "awesome_print"

app = Proc.new { |env|

  body = <<~HTML
    <form action='/'>
      <label> email </label>
      <input name='email' type='text'/>
      <input type='submit' value='Add email' />
    </form>
  HTML

  [
    20,
    {'Content-Type' => 'text/html'},
    [body]
  ]
}

run app

At this point, we can see our form rendered.

We also need a way to capture the email sent (if present). And there's where the env variable comes in.

The env variable contains a hash of information about the request, such as the HTTP host, path, IP address from where the site was requested, or, what interest us in this case, the query string.

Lets see how it looks like using the awesome_print gem.

# env["HTTP_HOST"]
# env["PATH_INFO"]
# env["REMOTE_ADDR"]
ap env["QUERY_STRING"]

When we submit an email

And we look at the console log, we see that it comes in as a string containing the name, then an equals sign and then the value.

The @ sign is replaced by %40. That's because it comes from the browser encoded with an encoding named URL encoding.

"email=fede%40example.com"
127.0.0.1 - - [10/Dec/2016:21:32:51 -0300] "GET /?email=fede%40example.com HTTP/1.1" 200 - 0.0024

Now that we know how it looks like, lets put it into a variable by splitting the string, and taking the email.

We can decode the URL encoding by calling the decode method on the URI module.

We append the latest entry to the EMAILS constant. And we initialize it with an empty array. We don't want to add an entry if we didn't get an email from the user.

Then, we create a list of all the emails that has been submitted. To accomplish this, we map all the gathered emails into an array of HTML list items. And then we join them. Finally, we add them to the template.

require "rack"
require "awesome_print"

EMAILS = []

app = Proc.new { |env|
  email = env["QUERY_STRING"].split("=").last

  EMAILS << URI.decode(email) if email

  emails_html = EMAILS.map {|e| "<li>#{e}</li>"}.join

  body = <<~HTML
    <form action='/'>
      <label> email </label>
      <input name='email' type='text'/>
      <input type='submit' value='Add email' />
    </form>

    <ul>
      #{emails_html}
    </ul>
  HTML

  [
   20,
   {'Content-Type' => 'text/html'},
   [body]
  ]
}

run app

Now we can try it out.

And that's it: we've created a Rack application from scratch. Yeah, we had to do a little more work than we would have if we had used a framework… but it's a surprisingly small amount of extra work, at least for this little app. And in return, we've programmed the whole HTTP request/response cycle without any mysterious "magic".

Rack is an extremely basic library that gets the job done. It's at the core of every major Ruby web framework. Some examples of Rack-compatible frameworks you might know are:

Even though I prefer using one of those frameworks instead of writing a plain Rack app, I find it important to understand it for two reasons.

First, because it gives us understanding of how our framework of choice works at deepest level.

And second, it allows us to extend our framework functionality by using Rack as middleware (which is a topic for another episode).

Happy hacking!

Responses