In Progress
Unit 1, Lesson 1
In Progress

Roda – Part 1

Sometimes Rails is overkill! For simple web applications or API servers, a lighter-weight web framework can be faster and have fewer moving parts than a full-fledged Rails app. Roda is one of the most elegantly designed lightweight web libraries I’ve seen, and in today’s episode Federico Iachetti is going to show you how to get started using it. Enjoy!


“For a comprehensive guide to developing web applications with Roda, check out Federico’s Roda Course!

Video transcript & code

Here's what a bare app in Rack looks like. This app captures emails and displays them on a list.

Using Rack for this tiny example is probably enough (see Episode 469 ), but sometimes we want just a leeeeetle bit more.

Today I come to show you a lightweight web framework called Roda.


  require "rack"
  require "awesome_print"

  EMAILS = []

  app = Proc.new { |env|
    email = env["QUERY_STRING"].split("=").last

    EMAILS << URI.decode(email) if email

    emails_html = EMAILS.map {|e| "<li>#{e}</li>"}.join

    body = <<~HTML
      <form action='/'>
        <label> email </label>
        <input name='email' type='text'/>
        <input type='submit' value='Add email' />
      </form>

      <ul>
        #{emails_html}
      </ul>
    HTML

    [
     20,
     {'Content-Type' => 'text/html'},
     [body]
    ]
  }

  run app

Roda is a routing tree web toolkit. It is not really a web framework, but a small Ruby library aimed at creating web applications.

Let's begin.

Roda website home-page

If you're used to working in a web framework like Rails, you might be expecting to invoking a special command to generate a new project. But, as said, Roda is more of a library than a framework.

So our first step is simply to create a new, empty directory. And we cd into it.


mkdir email_capture
cd email_capture

Let's add a Gemfile, to control the Rubygems this project will use and add the roda gem to it.


source "https://rubygems.org"

gem "roda"

After running bundle, we're ready to start writing some code.

Since Roda is a Rack compatible library, the first thing we need to do is create the config.ru file.

In it we require roda. We could create our Roda app right here in the config.ru file, but it's cleaner and easier to test if we separate it out into a file of its own. We'll call it app.rb.

Lastly, we'll add a call to run, passing the class of our App.


require "roda"
require "./app"

run App

We haven't defined App yet, so let's do that next.

We first create the App class, which inherits from Roda.

Roda is built around the idea of a routing tree, which means creating branches by adding routes.

Routing Diagram

Then we define a route block. This block will receive a Request as an argument, which we'll abbreviate as r by convention.

Our first route checks to see if the HTTP request is for the /emails path. If so, the block that we provide will be executed.


class App < Roda
  route do |r|
    r.is "emails" do

    end
  end
end

We can now start our server by calling the rackup command.


rackup

The first thing we need to understand about these routing blocks is that they must return a string, anything other than a string will result in a 404 error.


require "lucid_http"

GET "/emails"
# => "STATUS: 404 Not Found"

Let me repeat that so it sinks in. If the block returns anything other than a string, no matter if it's nil, a number, a symbol or a pizza, if it's not a string it's a miss!


r.is "emails" do
  [
    nil,
    123,
    :a_symbol,
    Pizza.new
  ].sample
end

So we take the email list, and we make an unordered list from it.

We'll hardcode a couple of emails to show this off.


  EMAILS = [
    'fede@example.com',
    'avdi@example.com'
  ]

  class App < Roda

    route do |r|
      r.is "emails" do
        [
          "<ul>",
          EMAILS.map {|email| "  <li>#{email}</li>"},
          "</ul>",
        ].join("\n")
      end

    end
  end

Now, if we go to _http://localhost:9292/emails_, we should see the list of emails.

Except we don't. We still get a 404, even though we're returning string. What happened?.


require "lucid_http"

GET "/emails"
# => "STATUS: 404 Not Found"

This is another reminder that we're not building on a fancy web framework. If we want bells and whistles like automatic code reloading, we can have them! ... but we have to ask for them.

For a very simple way to reload the code every time we change it, we'll use the rerun gem.

(If you want a quick intro on the rerun gem, check out Episode 320)


rackup
rerun rackup

And now, when we reload, our list is there!


  require "lucid_http"

  GET "/emails"
  # => "<ul>\n" +
  #    "  <li>fede@example.com</li>\n" +
  #    "  <li>avdi@example.com</li>\n" +
  #    "</ul>"

Now we need a form to add our new emails, and we want it under /emails/form.

So we need to branch out our routing tree. For this, we first wrap the list in an r.root block, which will match the root path of this new sub-tree. And then we add the second branch using r.is. We'll just return the string "FORM" to try it out.


  r.is "emails" do
    r.root do
      [
        "<ul>",
        EMAILS.map {|email| "  <li>#{email}</li>"},
        "</ul>",
      ].join("\n")
    end

    r.is "form" do
      "FORM"
    end
  end

And now nothing works.


  require "lucid_http"

  GET "/emails/"
  # => "STATUS: 404 Not Found"

  GET "/emails/form"
  # => "STATUS: 404 Not Found"

And that's because we are still using r.is for the \"emails\" path. r.is is what Roda calls a terminal matcher. Which means that it's a leaf on the routing tree. r.is doesn't allow any branching under it.

Routing Diagram

For this non-terminal route,...

Routing Diagram

...we need a non-terminal matcher, so we exchange r.is for r.on.


  # ...
  r.on "emails" do
  # ...

And now everything works again.


  require "lucid_http"

  GET "/emails/"
  # => "<ul>\n" +
  #    "  <li>fede@example.com</li>\n" +
  #    "  <li>avdi@example.com</li>\n" +
  #    "</ul>"

  GET "/emails/form"
  # => "FORM"

Now we can write our form.

We use the same /emails route as the list, but using the POST HTML verb.


  r.is "form" do
    <<~EOF
      <form action="/emails" method="post">
        <label for="email">Email</label>
        <input name="email" type="text" value=""/>
      </form>
    EOF
  end

In order to make the POST work, we first wrap our first two routes in an r.get block. This way we'll only be able to access those using GET.


r.on "emails" do
  r.get do
    r.root do
      # ...
    end

    r.is "form" do
      # ...
    end
  end
end

Then we add an r.post branch to catch our email. Inside the block, we fetch the email from the params hash, which is sent on the request. Then we append it to our email list. Finally we redirect to the list path in order to see the results.


r.post do
  EMAILS << r.params["email"]
  r.redirect "/emails/"
end

It's time to try it all out. We navigate to http://localhost:9292/emails/ and we see our email list. Then we go to the form, enter a new email and when we press enter, we get redirected to the /emails path. And we see the new email was effectively added. Now let's take a look at our resulting code, with the handler blocks .


  EMAILS = [
    'fede@example.com',
    'avdi@example.com'
  ]

  class App < Roda

    route do |r|
      r.on "emails" do
        r.get do
          r.root do
            [
              "<ul>",
              EMAILS.map {|email| "  <li>#{email}</li>"},
              "</ul>",
            ].join("\n")
          end

          r.is "form" do
            <<~EOF
              <form action="/emails" method="post">
                <label for="email">Email</label>
                <input name="email" type="text" value=""/>
              </form>
            EOF
          end
        end

        r.post do
          EMAILS << r.params["email"]
          r.redirect "/emails/"
        end
      end

    end
  end

We can see that there is an easy to follow path showing us the way Roda will route a request.

Routing Diagram

This is Roda in a nutshell, but we've barely scratched the surface of what it can do. We'll get deeper into Roda's features in a future episode. Happy hacking!

Responses