Building Web Apps with Roda
Roda is a Ruby web framework that takes a different approach to routing. Instead of matching routes one by one, Roda uses a routing tree that organizes routes hierarchically. This design makes your application faster and your routing code more organized.
Why Roda?
If you have used Rails or Sinatra, you might expect a flat list of routes. Roda changes this by letting you nest routes based on their path segments. Consider a typical blog application:
class BlogApp < Roda
route do |r|
r.root do
@articles = Article.latest(10)
view "home"
end
r.on "articles" do
r.on Integer do |article_id|
@article = Article.find(article_id)
r.is do
view "article/show"
end
r.on "edit" do
view "article/edit"
end
end
r.is do
view "articles/index"
end
end
end
end
This routing tree processes only the segments relevant to each branch. When a request comes to /articles/42, Roda skips the root and article index branches entirely, going straight to the integer segment match.
Installation and Setup
Install Roda with bundler:
gem "roda"
Create a minimal application:
# app.rb
require "roda"
class App < Roda
route do |r|
r.get "hello" do
"Hello, World!"
end
end
end
App.run!
Run it with ruby app.rb and visit http://localhost:9292/hello.
The Routing Tree in Detail
The routing block receives a request object r that you use to match path segments. Each method on r corresponds to an HTTP verb:
r.get— matches GET requestsr.post— matches POST requestsr.put/r.patch— match PUT and PATCH requestsr.delete— matches DELETE requests
The key methods for building your tree:
| Method | Purpose |
|---|---|
r.on | Adds a segment constraint, continues matching |
r.is | Matches the remaining path exactly |
r.root | Matches the root path / |
r.params | Accesses query and body parameters |
Understanding Segment Matching
Each r.on call consumes exactly one path segment:
r.on "users" do # matches /users
r.on Integer do |id| # matches /users/123
# id is now 123
end
end
The segment matchers are flexible:
# Match a specific string
r.on "admin" do; end
# Match a numeric segment
r.on Integer do |n|; end
# Match any segment (captures it)
r.on String do |segment|; end
# Match with a regex
r.on /\A(\w+)\z/ do |match|; end
Request and Response
The request object provides access to everything about the incoming HTTP request:
r.get? # true for GET requests
r.post? # true for POST requests
r.params # Hash of query and body parameters
r.headers # Hash of HTTP headers
r.body # Request body as IO
r.ip # Client IP address
r.url # Full request URL
r.path # Request path string
Setting the response is straightforward:
r.response.status = 201
r.response["X-Custom-Header"] = "value"
r.response["Content-Type"] = "application/json"
For convenience, you can return values directly from the route block:
r.get "data" do
{ json: "data" }.to_json
end
Plugins: Extending Roda
Roda stays small by moving most features into optional plugins. Include them with plugin:
class App < Roda
plugin :render
plugin :json
plugin :sessions,
key: "_app_session",
secret: ENV["SESSION_SECRET"]
end
Essential plugins for most applications:
- render — Template rendering with support for ERB, Haml, Slim
- json — JSON request parsing and response helpers
- sessions — Cookie-based session management
- cookies — Cookie reading and writing
- halt — Stop request processing early
- flash — Flash message support
Rendering Views
With the render plugin:
class App < Roda
plugin :render
route do |r|
r.get "about" do
view "about"
end
end
end
This looks for views/about.erb by default. Customize the directory:
plugin :render, views: "app/views", engine: "erb"
Common Gotchas
Path Matching Does Not Skip Middleware
Unlike Sinatra, Roda routes run after middleware. If you need path-specific middleware, handle it in the routing tree:
class App < Roda
use Rack::Session::Cookie, secret: "secret"
route do |r|
r.on "api" do
# Path-specific logic inside the block
# For authentication, use Rack middleware at the top level
# or check r.headers["Authorization"] here
# API routes here
end
# Regular routes here
end
end
Blocks Execute on Every Request
The route block runs for every request. Avoid expensive computations at the top level:
# Bad — database query on every request
route do |r|
@all_users = User.all
# ...
end
# Good — query only when needed
r.on "users" do
@users = User.all # only for /users routes
end
Matching Order Matters
Roda processes the routing tree top-down. More specific routes should come before general ones:
# Wrong — "users" matches Integer branch first in some cases
r.on Integer do |id|
# matches /123
end
r.on "users" do
# may never match if integer catches it
end
# Correct — specific paths first
r.on "users" do
r.on Integer do |id|; end
end
r.on Integer do |id|; end
A Complete Example
Here is a simple REST API with JSON responses:
require "roda"
class ApiApp < Roda
plugin :json
plugin :halt
@@articles = [
{ id: 1, title: "First Post" },
{ id: 2, title: "Second Post" }
]
route do |r|
r.on "articles" do
r.is do
# GET /articles — list all
r.get { @@articles }
# POST /articles — create
r.post do
id = @@articles.last[:id] + 1
article = { id: id, title: r.params["title"] }
@@articles << article
r.response.status = 201
article
end
end
r.on Integer do |id|
# GET/PUT/DELETE /articles/:id
r.get { @@articles.find { |a| a[:id] == id } }
r.put do
article = @@articles.find { |a| a[:id] == id }
article[:title] = r.params["title"] if article
article
end
r.delete do
@@articles.reject! { |a| a[:id] == id }
""
end
end
end
r.on "health" do
r.get { { status: "ok" } }
end
end
end
ApiApp.run!
Conclusion
Roda’s routing tree approach offers a refreshing alternative to traditional flat-route frameworks. By organizing routes hierarchically, you get faster request handling and cleaner code structure. Start with the basics — r.on for branches, r.is for leaf nodes — and add plugins only when you need them. The framework stays out of your way until you explicitly ask it to help.
See Also
- Rails Routing — Understanding RESTful routes in Rails
- Hanami Actions and Views — Another Ruby framework with action-based routing
- Rails API Mode — Building JSON APIs with Rails