It’s time for something new

After 14 years we’re hanging up our keyboards. Our team is joining the lovely people at Culture Amp, where we’ll be helping build a better world of work.

Icelab

Put HTTP in its place with Roda

By Tim Riley24 May 2016

Every app has boundaries. We encounter them when the app needs to interact with users, the outside word, or systems like databases. Building carefully around our boundaries is important. Do it well and we have an app split into neat layers, an app that is easy to change. Do it poorly and we leak details over these boundaries and make change much harder.

As web developers, we encounter one boundary extremely quickly: the HTTP request! HTTP brings a bounty of peculiarities that we’ll want to cordon off from the rest of our app. Our app is best arranged if we consider HTTP just one of many potential interfaces, and for this to work we need to keep our app’s core standing apart from any HTTP handling.

What we want, then, is a tool that can gracefully handle HTTP on one side and interact with our app on the other. Good friends, I know such a tool. Let me introduce you to Roda.

An expressive, flexible HTTP toolkit

Roda is a “routing tree web toolkit” written by Ruby community stalwart Jeremy Evans. The routing tree is one of the most expressive approaches to routing I’ve seen, and it’s best demonstrated with an example:

class MyApp < Roda
  route do |r|
    # Branch on "/hello"
    r.on "hello" do
      @greeting = "Hello"

      r.is do
        # Respond to GET /hello
        r.get do
          "#{@greeting}!"
        end
      end

      r.on ":recipient" do |recipient|
        # Respond to GET /hello/:recipient
        r.get do
          "#{@greeting}, #{recipient}!"
        end
      end
    end
  end
end

Roda processes each incoming request through this route block, branching by various matchers into increasingly specific routing blocks until a response is returned. This allows for a truly concise, single-glance approach to routing. Context for a particular branch of the routes can be established just once (like @greeting above) and can be used anywhere within it.

The other notable thing about Roda is that what you see above is exactly what you get: Roda has a very slim core. It offers a plugin system for adding features or enhancing behaviour. This is Roda’s “web toolkit” side.

This plugin system is the key for us to neatly place Roda on top of our app as its web interface. We can write a plugin to make the objects in our app’s container readily available to use within Roda routes.

A distinct HTTP interface for our app

This is exactly what dry-web provides for you out of the box. dry-web is one of the dry-rb project’s more fledgling gems, but we’re already using it successfully here at Icelab. At the moment, it integrates only with Roda, but we’ll soon be adding support for other frameworks like Hanami.

To get started with dry-web and Roda, we first want to set up our app’s container:

class MyContainer < Dry::Web::Container
  configure do |config|
    config.root = Pathname(__FILE__).join("../..").realpath.dirname.freeze
    config.auto_register = ["lib"]
  end

  load_paths! "lib"
end

Then we can use it from within Roda:

class MyApp < Dry::Web::Application
  configure do |config|
    config.container = MyContainer
  end

  route do |r|
    r.on "articles" do
      r.is do
        r.view "views.articles.index"
      end

      r.post do
        r.resolve "operations.create_article" do |create_article|
          # In reality, we'll want to handle success or failure differently
          # here, but this is a topic for another time.
          create_article.(r[:post])
          r.redirect "/articles"
        end
      end
    end
  end
end

Here we see a couple of different ways Roda interacts with our app. r.view fetches an object from the container and sends #call to it right away, expecting that object to return HTML that can be sent back to the browser. Rendering views like this happens frequently enough that we want to enable it to happen with minimum ceremony, which Roda’s plugin system allows us to do.

The other thing we see is that we can resolve objects from the container and make them available as variables for us to work with directly inside a route block. In this case, when we make a POST /articles request, we want to fetch our functional command object and make it available to #call with the parameters for the new article. Working with functional objects is a real advantage here: they’re focused around actions (which lines up nicely with HTTP requests), they offer a single, clear interface and return well-defined results.

With these two interfaces within Roda’s routes, we now have a clear way to interact with our app’s objects. Our app’s objects also don’t need to concern themselves with HTTP-related issues at all, since that can sit entirely within Roda’s layer. It’s solely within Roda that we handle things like request parameters, authentication, cookies, redirect or render responses, and anything else HTTP-related.

The end result? Our app’s objects can remain focused on our app’s core logic, and we have a clearly defined place to handle HTTP as an interface to our app. We respected the HTTP request as a boundary and have built cleanly around it. We can make changes to our app’s core logic without concerning ourselves with HTTP details, and we can change how the HTTP interface operates without any flow-on effects to the rest of the app. This makes our app easier to understand and ensures we can make changes with confidence!

Learning more

This is just the start of our explorations with dry-web, Roda, and our app’s HTTP boundary. In the meantime, if you want to learn more more about Roda, check out Janko Marohnić’s excellent introduction. And if you’d like to see a real-life example of dry-web and Roda in action, you can explore Icelab’s dry-web skeleton and berg, our new company website we’re building with these tools.

We’ll be visiting this topic plenty more in the future, but for next week, we’ll look at another important boundary in our apps: the database. That’s right, we’ll be getting acquainted with ROM! I can’t wait.


Read part 1 of this series: My past and future Ruby

Part 2: Inactive records: the value objects your app deserves

Part 3: Functional command objects in Ruby

Part 4: Effective Ruby dependency injection at scale

Part 5: Better code with an inversion of control container

Part 6: A change-positive Ruby web application architecture