Building Web Apps with Roda

· 5 min read · Updated March 18, 2026 · beginner
roda ruby web development framework routing

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 requests
  • r.post — matches POST requests
  • r.put / r.patch — match PUT and PATCH requests
  • r.delete — matches DELETE requests

The key methods for building your tree:

MethodPurpose
r.onAdds a segment constraint, continues matching
r.isMatches the remaining path exactly
r.rootMatches the root path /
r.paramsAccesses 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