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.
GET users/1/invoices
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::
(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.
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.
mkdir my_roda_app
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.
bundle install
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.
Rack documentation
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 Roda
class.
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 run
the App
class.
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.
rackup
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.
r.on
methodThe 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
We called r.on
with the string users
, which will match the current request path if the the request path starts with users
.
The 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
(e.g., |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 users
branch.
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), r.on
returns 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.is
methodr.on
will match all request paths starting with users
, including 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
and r.is
methods. 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 /users/list
. r.on
creates a branch that handles all paths under users
, and r.is
creates a node that matches only if the current path is list
. 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.
r.get
Routing will usually take the request method into account as well. While r.on
and 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
A GET users/list
request would return “Hello Users!”
r.post
While r.get
matches GET
requests, r.post
matches POST
requests.
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
A POST users/list
request would return “User added.” r.get
and r.post
are used to handle various request methods on similar request paths.
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.
Jeremy Evans on Github
RubyConf 2014 – Roda: The Routing Tree Web Framework by Jeremy Evans
Mastering Roda
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.