Skip to content

Instantly share code, notes, and snippets.

@scalaview
Forked from markbates/gist:4240848
Created August 14, 2021 15:21
Show Gist options
  • Save scalaview/b0e6e3f76418f1a87e9c1b63e9bfffcb to your computer and use it in GitHub Desktop.
Save scalaview/b0e6e3f76418f1a87e9c1b63e9bfffcb to your computer and use it in GitHub Desktop.
Getting Started with Rack

If you're writing web applications with Ruby there comes a time when you might need something a lot simpler, or even faster, than Ruby on Rails or the Sinatra micro-framework. Enter Rack.

Rack describes itself as follows:

Rack provides a minimal interface between webservers supporting Ruby and Ruby frameworks.

Before Rack came along Ruby web frameworks all implemented their own interfaces, which made it incredibly difficult to write web servers for them, or to share code between two different frameworks. Now almost all Ruby web frameworks implement Rack, including Rails and Sinatra, meaning that these applications can now behave in a similar fashion to one another.

At it's core Rack provides a great set of tools to allow you to build the most simple web application or interface you can. Rack applications can be written in a single line of code. But we're getting ahead of ourselves a bit.

Installing Rack is simple. You just need to install the rack gem.

gem install rack

I would also recommend installing a Rack compatible server, such as thin, but Webrick should work just as well.

gem install thin

With Rack installed let's create our first Rack application. Like I said Rack applications can be written in just one line of code, should we desire.

run ->(env) { [200, {"Content-Type" => "text/html"}, ["Hello World!"]] }

We can run this application like such:

rackup lambda_example.ru

If we now visit http://localhost:9292 we should be greeted with Hello World! as we expect.

So how did this work? Rack has two simple requirements. The first is that whatever you ask it to run must respond to a call method, which is why a lambda or Proc works here. This call method will be called with a Hash-like object that contains the request, as well as other environmental data.

The call method must return an Array that contains three elements.

The first element is an integer that represents the status of the request, so in our example we return a status of 200.

The second element is a Hash that contains any headers you want to return with the response.

The last element is the body of the response. Whatever object you return here must be an enumerable object, such as an Array or an IO object.

Since Rack will accept any object that responds to a call method we can enhance our example further by using a full Ruby class instead of just a lambda.

class HelloWorld
  def call(env)
    [200, {"Content-Type" => "text/html"}, ["Hello World!"]]
  end
end

run HelloWorld.new

We're now doing the same thing, but we are using a class instead of a lambda.

As I said earlier Rack provides a great toolkit for writing Ruby applications. So let's use a little bit of that in this application. Let's add a few routes.

class HelloWorld
  def call(env)
    req = Rack::Request.new(env)
    case req.path_info
    when /hello/
      [200, {"Content-Type" => "text/html"}, ["Hello World!"]]
    when /goodbye/  
      [500, {"Content-Type" => "text/html"}, ["Goodbye Cruel World!"]]
    else
      [404, {"Content-Type" => "text/html"}, ["I'm Lost!"]]
    end
  end
end

run HelloWorld.new

First we create a new Rack::Request object using the env hash that was passed in from the request. We can then use the path_info attribute on the request and build a simple case statement to handling our "routing".

If the path matches /hello/ we'll return a status of 200 and the body of "Hello World!". If the path matches /goodbye/ we'll return a status of 500 and the body of "Goodbye Cruel World!". All other requests will get a 404 response.

Now if we open a browser and navigate to http://localhost:9292 we should see the 404 page because none of our routes matched. We can confirm that status of 404 in the browsers inspector window.

Navigating to http://localhost:9292/hello and http://localhost:9292/goodbye give us the results we would expect.

Fantastic, we now have some simple routing in our application, however, this won't scale very well, and case statements aren't going to get us where we need to be.

Let's take this example one step further and build a simple web framework that will handle GET requests. I'll leave it up to you to support the other HTTP protocols.

Let's start with what we want the application to look like, and then we'll fill in the details when we write our simple framework.

$:.unshift File.dirname(__FILE__)
require 'simple_framework0'

route("/hello") do
  "Hello #{params['name'] || "World"}!"
end

route("/goodbye") do
  status 500
  "Goodbye Cruel World!"
end

run SimpleFramework.app

The first few lines are simply there to add the current directory to Ruby's load path and require, what will be, our framework.

Our framework, named SimpleFramework, gives us a route method that takes a path and a block. If the request matches the path then the block will be evaluated and the last line of the block will be the body of the response.

In the hello block you can see we are referencing a params hash, so we'll need to make sure that is available to the block. In the goodbye block we want to set the status of the response to 500, so we'll need a status method that let's us do that.

Now that we know what we want the SimpleFramework to look like, let's actually implement it.

require 'action'
class SimpleFramework

  def self.app
    @app ||= begin
      Rack::Builder.new do
        map "/" do
          run ->(env) {[404, {'Content-Type' => 'text/plain'}, ['Page Not Found!']] }
        end
      end
    end
  end

end

def route(pattern, &block)
  SimpleFramework.app.map(pattern) do
    run Action.new(&block)
  end
end

The first thing we are doing is requiring a filed named action, we'll look at that in just one second. Let's look at the SimpleFramework class first.

The SimpleFramework class is quite simple, thanks to the the Rack::Builder class that Rack offers us. The app method we've defined will return an instance of the Rack::Builder class. Rack::Builder let's us easily construct a Rack application by letting us chain together a bunch of smaller Rack applications.

In the app method when we create the instance of the Rack::Builder class we are going to map a default Rack application to run, should no other path match.

Finally we define a route method at the root object space to let us map our actions.

The real meat of SimpleFramework is the Action class, and even that is pretty simple and straight forward.

class Action
  attr_reader :headers, :body, :request

  def initialize(&block)
    @block = block
    @status = 200
    @headers = {"Content-Type" => "text/html"}
    @body = ""
  end

  def status(value = nil)
    value ? @status = value : @status
  end

  def params
    request.params
  end

  def call(env)
    @request = Rack::Request.new(env)
    @body = self.instance_eval(&@block)
    [status, headers, [body]]
  end

end

When we initialize a new instance of the Action class we define a few attr_reader methods and set them to some basic defaults.

We implement the status and params methods so we have access to them as we saw earlier.

Finally, we implement a call method that takes in the env hash and creates a Rack::Request object from it. We then use instance_eval to evaluate the original block in the context of the Action object so it has access to all of the methods and goodies in the Action class.

All that is left to do is return the appropriate Rack response Array.

When we navigate to http://localhost:9292/hello in the browser we see Hello World! just as expected. If we pass name=Mark on the query string we should now see Hello Mark! proving that we do have access to the request parameters, just like we wanted.

A quick look at http://localhost:9292/goodbye confirms that we are getting a status of 500 and the text of Goodbye Cruel World!.

Well, that's a quick look at the basics of the Rack library. There is a lot more to it, and I highly suggest you read its very thorough documentation.

One word of caution though, from someone who's been there, it is very easy to go down this path and end up building your own fully featured framework. Before you do that, make sure that something like Sinatra, doesn't already float your boat.

That's it, for now. I hope this helps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment