Roda is a Ruby web framework created to bring the best of Sinatra and Cuba into Ruby web development. Roda was designed around the concept of a “routing tree.”
In web development, routing is the process of taking a request, then finding the code that would handle that request. Consider an HTTP request path like the one below.
When deciding how to handle a route, a typical web framework (e.g., Ruby on Rails or Sinatra) looks at the full request path, iterating an array of possible routes to match the request.
On the other hand, a routing tree framework such as Roda checks each segment of the request path before continuing.
In handling the request above, Roda looks at the first segment,
/users. If this segment does not match the request, it skips the entire
/users branch so that the other routes under
/users are not considered. If
/users matches, it is going to look into the
/users branch for
/1. It continues this way until the route is found, or otherwise returns a
404(not found) status if the route cannot be matched.
Since route handling does not involve iterating through an array of routes, per-request overhead is significantly lower when compared to other Ruby frameworks.
What makes a routing tree interesting is not just performance, but that at any point during routing, you can operate on the current request.
Roda is designed to be simple, both internally and externally. It uses a routing tree to enable you to write simpler and DRYer code.
Roda has very little global namespace pollution. All instance variables used internally in the routing tree are prefixed with an underscore (
@_variable), so they do not conflict with the instance variables the user wants to use. Additionally, Roda prefixes constants with
Roda::constant) to avoid conflicting with the constants that you use for your application.
Roda is extensible via broad plugin support. You can override any part of Roda and call
super to get the default behavior.
A simple routing tree
Let’s start off by creating a very small web application to understand what working with Roda looks like. Roda is more of a Ruby library than a framework, and does not have CLI utility commands for performing regular tasks. So unlike Rails and other frameworks, it does not have commands for creating a new project, generating models, controllers, and other command-repetitive tasks.
Let’s create a directory to hold our project files.
Then we add a
Gemfile for managing the gems we will use. Add the
roda gem into the file, and then
puma, which will be our web application server. Our
Gemfile will look like this.
source "https://rubygems.org" gem "roda" gem "puma"
Now we run
bundle install to install the gems we’ve added.
Most Ruby web frameworks were built on top of Rack, which, according to Rack’s documentation, is a modular Ruby web server interface.
Rack provides a minimal, modular, and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.
Roda is Rack-compatible, so we start out by creating a rackup file using the standard filename
config.ru. In it we require
roda, and then create a new class to represent our application. This app will inherit from the
Because Roda is built on top of Rack, every class that inherits from
Roda implicitly becomes a Rack application. In order to tell Rack (and the web server) to execute our app for HTTP requests, we have to tell it to
require "roda" class App < Roda route do |r| "Hello World!" end end run App
Then from the command line, we run the
rackup command to start up the web server and begin serving requests.
Open the browser and navigate to http://127.0.0.1:9292 to see “Hello world!”
Our app at its current state returns “Hello world!” as the response for every request route. Let’s introduce Roda’s routing method to make the app return the same response, but for only a specific request path and method.
Roda routing tree methods
The first routing tree method is
r.on, which creates branches in the routing tree. Let’s add
r.on to our example app.
require "roda" class App < Roda route do |r| r.on 'users' do "Hello Users!" end end end run App
r.on with the string
users, which will match the current request path if the the request path starts with
Roda.route method is the start of the routing tree. All requests coming into our application will be yielded into the block passed into
Roda.route. The block is yielded a route request instance with some additional methods. Conventionally, the block argument is named
|r|). The additional methods (e.g.,
r.on ) passed to the route request instance are used for routing the request.
Route methods accept arguments called matchers, which are used to match the current request. In our example above, we passed a string matcher
users as an argument to
r.on to create the
In Roda, string matchers usually match the first segment of the request path. So if the request path starts with
users, it matches and is redirected to the passed-in block, which returns the “Hello Users!” string that Roda is using as the response body.
If the request path starts with
profile (which does not match),
nil without yielding to the block, and execution would continue after the call. If no other block exists, which is the case in our example above,
Roda.route will return
nil and Roda will return a
404 status code with an empty response body.
r.on will match all request paths starting with
users/non-existent-user. Usually, this is not what we want. Instead, we want to return a
404 response for any path that has not been specifically handled.
This is where
r.is comes in handy. It only matches a path if all of the arguments match and there are no further entries in the path after matching. In other words, it finalizes the routing path.
r.is will only match
users and not
users/non-existent-user. Routing in Roda is done using a combination of the
r.on does prefix matching of the request path, and
r.is does full matching of the request path. So while
r.on creates branches,
r.is creates leaf nodes.
Let’s add a
r.on to our example.
require "roda" class App < Roda route do |r| r.on 'users' do r.is "list" do "Hello Users!" end end end end run App
Our app will match any request with the path
r.on creates a branch that handles all paths under
r.is creates a node that matches only if the current path is
r.is is able to match
list because the request path is modified as the request is being routed.
When the request for
users/list comes in, the routing tree uses the initial request path
users. When the
r.on "users" request method matches, it consumes
users from the front of the request path. Inside the
r.on block, the request path is empty.
In the next
r.is block, the current path is
list and will only match if all of its matchers agree throughout the request path. So in this example case, the request will be matched and “Hello Users!” will be returned.
Routing will usually take the request method into account as well. While
r.is focus on routing using the request path,
r.get is for routing based on the
GET request method. If it is invoked without a matcher, it puts a simple match against the request method. If invoked with a matcher, a terminal match is performed against the request path.
require "roda" class App < Roda route do |r| r.on 'users' do r.is "list" do r.get do "Hello Users!" end end end end end run App
GET users/list request would return “Hello Users!”
require "roda" class App < Roda route do |r| r.on 'users' do r.is "list" do r.get do "Hello Users!" end r.post do "User added" end end end end end run App
POST users/list request would return “User added.”
r.post are used to handle various request methods on similar request paths.
Code execution during routing
Roda has the ability to execute code during the routing process. This makes it easy to write simpler and DRYer code and avoid code duplication.
If you want to check whether someone is logged in before accessing a route, you can put the code that checks for login as the first line in the
Roda.route block. This is useful when dealing with separate request methods for the same request path. For instance, a
GET request will find a user and return their details, and a
POST request will find a user and update their details.
We can abstract the logic for finding the user to a routing block shared by both request methods as shown below.
class App < Roda route do |r| require_login! r.on 'users' do r.is ":id" do |user_id| @user = User.find(user_id) r.get do @user end r.post do @user.update(params[:user]) end end end end end run App
Roda is designed to be lightweight and ships only with essential features to get your app quickly running. All additional features are loaded via plugins; Roda ships with over 60 built-in plugins, all of which are maintained with the same level of quality as Roda itself, so you’ll rarely need external ones.
LogRocket: Full visibility into your web apps
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.