Ruby is an amazingly expressive scripting language that has gained, largely due to Ruby on Rails, a huge following and developer ecosystem. So much so that a lot of people struggle to detangle Ruby (the programming language) from Rails (the framework). But for the new, or even seasoned, Ruby developer there’s a healthy market of non-Rails frameworks out there.

Having built web apps in Ramaze, Sinatra, Grape (API centric), Rails I now continue to be blown away - after 2 years - by the relative newcomer that is Roda. Originally forked from the Cuba microframework, Roda has been nurtured and furthered by Jeremy Evans to become a beautifully powerful web or API framework. If that name sounds familiar, he is also the maintainer of the Sequel ORM and numerous other highly visible Ruby and OpenBSD projects.

Why Roda

This post, and subsequent ones, isn’t about Roda vs. Rails/Sinatra/Hanami. It’s about why you might want to choose to use Roda, and helping you on that journey. Most mature Ruby web frameworks can be used to build APIs/CRUDs/CMSs and it’s very unlikely your users care one bit about your framework assuming the thing keeps running.

Roda is a great choice if any of these reasons resonate:

  • Start with a small, compact core of an application and grow complexity over time and learning as you go
  • Have fine grained control on features and performance
  • Use a routing-tree approach to represent your app instead of being persuaded towards index/view/edit controller routes
  • You don’t want to be overwhelemed with generator/boiler-plate code

Again, this isn’t a better-or-worse comment on Ruby web frameworks, but rather a journey into the fast and powerful Roda!

Your First Roda App

To get started we’re going to create a super simple app with the following directories and files:

1-first-app
├── Gemfile
├── app.rb
├── config.ru
├── routes
│   └── first-route.rb
└── views
    ├── index.erb
    └── layout.erb

You can copy and paste the lines below to re-create this structure:

mkdir 1-first-app && cd 1-first-app
mkdir views routes
touch Gemfile app.rb config.ru routes/first-route.rb views/layout.erb views/index.erb

It’s possible to create a one-file Roda app (config.ru) but this isn’t representative of an app you’d grow over time!

With the above structure created, open up Gemfile in your editor of choice and update it to match per below:

source 'https://rubygems.org'

gem 'roda', '~> 3.22'           # Roda web framework
gem 'tilt', '~> 2.0.6'          # Templating engine
gem 'erubi', '~> 1.5'           # Template syntax
gem 'puma', '~> 4.0'            # Web application server
gem 'rack-unreloader'           # Allows code reloading

I’ve added comments, but to briefly summarise - roda is the web framework, tilt is a generic interface to work with multiple template languages/syntaxes, erubi is an ERB implementation by Jeremy Evans (ERB is the defacto ruby template language supported in Ruby’s standard library), puma is the web application server (what Apache or Nginx would talk to) and rack-unreloader reloads modified code so you can refresh the browser and see code changes without having to cycle your app.

Once you’ve installed your gems with bundle install, open up app.rb so we can create the core of this first, basic app.

Setting up the Roda App

Update your app.rb file to look like the below:

require 'roda'
require 'tilt/sass'

class App < Roda
  plugin :render, escape: true
  plugin :hash_routes

  Unreloader.require('routes') {}

  route do |r|
    r.hash_routes('')
  end
end

So, what’s happening here? We’re greating a Class that inherits from Roda - this App class is what we’re going to later run via config.ru to serve the application.

You can see we’re loading a few plugins to introduce optional functionality to the app. As mentioned early, Roda allows you to build powerful and complex apps, but most functionality gets loaded via plugins to keep things lean.

The render plugin is Roda’s interface to the tilt library, and is responsible for rendering views and templates. Note that the plugin defaults to use the layout template defined in views/layout.erb unless you override it.

The hash_routes plugin is what we’re using to route URL paths to different blocks (i.e. bits of code). This lets us split our Controller logic into separate files and load them using Unreloader.require('routes') {} (this loads Ruby files in the routes directory). We’ll see shortly how the controller looks, and routing will start to make more sense.

Finally we set up the “master” route, and tell Roda to use the hash_routes plugin to route requests. Note that historically you might use the multi_route plugin to map routes to controller code, but hash_routes effectively supercedes it and offers more - and faster - routing features.

Roda Route + Controller

Even though Roda could be spun up in a single config.ru file and run with Rack, it doesn’t really help setup a structure for a real world app. So now you can open up routes/first-route.rb and update it to match the below:

class App
  hash_branch 'first-route' do |r|
    r.on 'hello' do
      r.get do
        @title = "Hello World"
        view 'index'
      end
    end
  end
end

Here we have a very basic route and controller! The hash_branch method block goes hand-in-hand with the hash_routes invocation and tells Roda to use this block to handle web requests that start /first-route.

With the above block, attempting to access /first-route would give you a blank page and a 404 response code (we’ll get to proper error pages in a future post). However if you GET access to /first-route/hello you’ll get the contents of views/index.erb rendered through views/layout.erb with a @title class variable set as “Hello World”.

Remember that the the render plugin is defaulting to view/layout.erb. You could override an individual view method call with:

view 'index', layout: 'views/alternative-layout.erb'

A basic Layout + View

We’re not overly concerned with presentation here, so open up views/layout.erb and paste in the following:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title><%= @title || 'My First Roda App' %></title>
  </head>
  <body>
    <%== yield %>
  </body>
</html>

The yield keyword is where the content of your view will go - all fairly standard stuff if you’ve done any web app work before.

Our views/index.erb is going to be very simple…

<h2>Hello Roda!<h2>

We’re pretty much done - we just need to tell Rack (and Puma) how to load and run the application.

Running a Roda App

dev = ENV['RACK_ENV'] == 'development'

require 'rack/unreloader'

Unreloader = Rack::Unreloader.new(subclasses: %w'Roda', reload: dev){App}
Unreloader.require('app.rb'){'App'}
run(dev ? Unreloader : App.freeze.app)

You can see here that we’re heavily leveraging Unreloader - this watches files and reloads the classes in the app if they’ve changed. It’s kind of like hot reloading, and makes development a much more sane experience. If you’re using other frameworks (e.g. Sequel) you’d pass in an extra class to subclasses so those classes are also reloaded.

The run keyword is a rack method call and is used by Puma to actually load and run the app.

Saving the above, then running bundle exec rackup in your terminal should fire up the app on (by default) port 9292 and you can access the route by heading to http://localhost:9292/first-route/hello

Note that you can also access /first-route/hello/foo/bar/blah and still get the same result. Why?! Well, our r.get directive under r.on 'hello' matches everything starting with /first-route/hello. If you change r.on 'hello' to r.is 'hello' you tell Roda to use an “exact match” on the routing tree and it becomes very strict. The verbs and routing tree take a little getting used to, and you may find it overly fussy moving between on and is and may choose to stick to one or the other.

If you’re using the r.is you probably want to update your plugins to use plugin :slash_path_empty. This route matching method is an exact match and won’t match if anything is trailing the match. For example:

# Without slash_path_empty
r.is 'foo'        # /foo will match, /foo/ will 404

# With slash_path_empty
r.is 'foo'        # /foo will match, /foo/ will also match

Given you can’t stop a user typing in a URL, you probably want to avoid sending them to a 404 for the sake of a trailing slash!

A Note on Application Structure

What we’ve built above is obviously very basic, but matches quite similarly to the official conventions for structuring a larger app. One deviation is I prefer to use app.rb for the application file as it reduces cognitive load when searching and opening the file.

We’re still missing a few key elements that we’ll step through in a future post, though:

  • Style and Script assets - Roda has a great :assets plugin which can pre-compile and cache Sass and Javascript (and more)
  • Models and Database connectivity (and migrations)
  • Helper modules/classes to help keep our app DRY
  • Test files

I’ll be covering up more Roda in a future post!