Rack and Ring Basics

In the Clojure world, when you want to write a web app, you will almost certainly be using Ring. Ring is directly inspired by Rack, the defacto webserver abstraction for frameworks in Ruby.

The two are conceptually very similar, but there are some slight differences (aside from language) that might be interesting to highlight. I won't be going too in depth with this post, and will focus instead on the basics of using Rack and Ring. I thought I would just get that out of the way before your expectations got too high.

It's worth noting that most apps won't actually use either Rack or Ring directly like we will here. It would be a lot faster and safer to use something like Sinatra or Rails for Rack, and Compojure or Luminus for Ring.

If you want to see the final versions of these examples in their full forms please click:

The Minimal First Step

Let's start with Rack, since it's obviously the more popular of the two. I'm going to assume that you can read and get Rack installed if you don't already have it. If you can't read, then I have nothing to worry about because this will all look like gobbledygook to you. I hope ASCII art will serve as an appropriate apology :-).

The first step in getting either application off the ground is to create a entry point. In the case of Rack this is will be an object with a call method. This method needs to return the basic structure required for a Rack response, which is an array containing the response status, headers and body:

    # my_rack_app.rb
    class MyRackApp
      def call(env)
        ['200', {'Content-Type' => 'text/html'}, ['Hello World']]
      end
    end

Note how simple this is. This is just a regular old ruby object with a method that returns a triplet. It knows nothing about Rack or anything else that might be using it.

Unfortunately this does absolutely nothing, which is pretty boring. To boot this app up, we'll add a config.ru to the current directory:

# config.ru
require 'rack'
require_relative 'my_rack_app'

run MyRackApp.new

We can now run rackup from our current directory and, navigating to http://localhost:9292, we should see our "Hello World" response.

Getting our Ring app off the ground requires a little bit more setup, but we'll be using Leiningen to do most of the trivial stuff for us.

First let's create a new project with lein new app my-ring-app. Add [ring "1.3.2"] to the list of dependencies in project.clj. You might need to run lein deps to download the Ring library if you don't already have it.

Ring is similar to Rack in that we need to give it a function that returns some standard response. Ring expects a map instead of an array:

    (ns my-ring-app.app)

    (defn handler [request]
      {:status 200
       :headers {"Content-Type" "text/html"}
       :body "Hello World"})

Calling the function "handler" above is simply the convention in Clojure-land. Like the Rack example, we need some way to boot this app up. Some examples will mash this boot process together with the code we wrote above but I prefer to keep this separate. Plus it makes it similar to our Rack example so win-win, right?

    (ns my-ring-app.core
      (:require [ring.adapter.jetty :refer [run-jetty]]
                [my-ring-app.app :as app)
      (:gen-class))

    (defn -main [& args]
      (run-jetty app/handler {:port 3000}))

Now we can just do lein run from our project root. Navigating to http://localhost:3000 should display a very familiar page.

Middleware

Middleware is essentially a series a steps a request has to go through in order to generate a response. Once a response is generated, it will return the response back up through any middleware in reverse order.

We're going to introduce some middleware to both of our applications that translates "Hello" to it's French counterpart "Bonjour". Why French? Well I'm Canadian, and if I don't provide some kind of French content, then the CRTC might come and yell at me for not being fair.

I also don't know how to say "Hello" in any other language off the top of my head and I'm too lazy to do any more research so we are going to stick with "Bonjour".

In Rack our middleware is going to be another ruby object, except that we will actually be doing something with that env parameter we saw in our earlier example. You will notice that our middleware bears some resemblance to our running MyRackApp

    # hello_translator.rb
    class HelloTranslator
      def initialize(app)
        @app = app
      end

      def translate_hello(str)
        str.gsub(/Hello/, 'Bonjour')
      end

      def call(env)
        status, headers, body = @app.call(env)
        body.map! { |str| translate_hello(str) }
        [status, headers, body]
      end
    end

First we initialize the middleware with our running application. Calling @app.call(env) simply passes the request down the stack. If there was another piece of middleware beneath us, then it would be the next reciever of our env parameter. This would keep going until a response is generated, which in our case will be from MyRackApp that we defined earlier. Our return value needs to be the same status, headers and body array in order for Rack to be able to serve the response.

If you ran rackup right now, then you wouldn't see any change. This is because we actually need to tell Rack about this is middleware. To accomplish this we need to require and use it in our config.ru:

    # config.ru
    require 'rack'
    require_relative 'my_rack_app'
    require_relative 'hello_translator'

    use HelloTranslator
    run MyRackApp.new

Running rackup and refreshing the previous page will now show a lovely greeting in the famously romantic French language.

Again Ring is very similar. The biggest difference is that the middleware is just a function instead of an object:

    (ns my-ring-app.middleware
      (:require [clojure.string :as s]))

    (defn translate-hello [body]
     (s/replace body "Hello" "Bonjour"))

    (defn wrap-hello-translator [handler]
      (fn [request]
        (let [response (handler request)]
          (update-in response [:body] translate-hello))))

Again, prefixing our middleware with wrap is convention. As before we will need to require and use this middleware in the namespace responsible for booting the app:

    (ns my-ring-app.core
      (:require [ring.adapter.jetty :refer [run-jetty]]
                [my-ring-app.app :as app]
                [my-ring-app.middleware :refer [wrap-hello-translator]])
      (:gen-class))

    (defn -main [& args]
      (run-jetty (-> app/handler
                     wrap-hello-translator)
                 {:port 3000}))

Dealing With Query Params

I was originally going to end the post here with an amazingly concise, beautifully written conclusion to tie a nice little bow over everything. Instead I'm going to take this comparison one step further and really phone in the whole "wrapping things up" section that is structurally necessary.

I want to show something that is much more common than our mostly pointless "Hello" to "Bonjour" translation app that we've written. I'm going to show you how Ring and Rack each let you access any query parameters that may have come along with the request.

Let's say that we want to let the request specify who we want to say "Bonjour" to, and "World" if nothing is provided. The request can tell us what name to use by simply providing a name parameter.

With the Rack example, we aren't going to touch anything except for our base application:

    # my_rack_app.rb
    class MyRackApp
      def call(env)
        request = Rack::Request.new(env)
        subject = request.params.fetch('name', 'World')
        body = ["Hello #{subject}"]

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

The change here is that we wrap the env parameter in a Rack::Request object, then attempt to fetch the 'name' parameter. If one isn't provided, then we default to 'World'. Our response is essentially the same.

Opening http://localhost:9292?name=Pierre, we should see "Bonjour Pierre". If we omit the name parameter entirely, then we should see "Bonjour World" as before.

Ring's approach is slightly different. Instead of wrapping the request in another object, there is a set of very common default middleware that we can choose to include. One of these is wrap-params, which will add a :params key to the request and take any parameters out of the query string and put into a map at that key.

I've included full-versions of each file here to keep things easy to follow. Additions to my-ring-app.core are noted:

    (ns my-ring-app.core
      (:require [ring.adapter.jetty :refer [run-jetty]]
                [ring.middleware.params :refer [wrap-params]] ; NEW
                [my-ring-app.app :as app]
                [my-ring-app.middleware :refer [wrap-hello-translator]])
      (:gen-class))

    (defn -main [& args]
      (run-jetty (-> app/handler
                     wrap-params ; NEW
                     wrap-hello-translator) {:port 3000}))
    (ns my-ring-app.app)

    (defn handler [request]
      (let [subject (get-in request [:params "name"] "World")]
        {:status 200
         :headers  {"Content-Type" "text/html"}
         :body (format "Hello %s" subject)}))

The difference between the two examples is the fact that the query string processing is handled in middleware with Ring, while with Rack I wrapped it. I tried to find a similar piece of middleware for Rack but after an hour of clicking around I came up short. There is some stuff in rack-contrib that comes close, but not close enough. I'm sure one exists and I'll update this post if I can find something analogous to Ring's wrap-params.

I hope this been at least a little bit enlightening. I found the basics of Ring and Rack to be so similiar that I couldn't help but write a post about the two. The differences in application structure and methodologies become more pronounced the more complicated your stack becomes, but this is usually due to stuff built on top of them.

In future posts I hope to show something a bit more useful built with Ring.

Published: 2015-04-15