A Survey of Non-Rails Frameworks in Ruby: Cuba, Sinatra, Padrino, Lotus

Written by: Leigh Halliday

It's common for a Ruby developer to describe themselves as a Rails developer. It's also common for someone's entire Ruby experience to be through Rails. Rails began in 2003 by David Heinemeier Hansson, quickly becoming the most popular web framework and also serving as an introduction to Ruby for many developers.

Rails is great. I use it every day for my job, it was my introduction to Ruby, and this is the most fun I've ever had as a developer. That being said, there are a number of great framework options out there that aren't Rails.

This article intends to highlight the differences between Cuba, Sinatra, Padrino, Lotus, and how they compare to or differ from Rails. Let's have a look at some Non-Rails Frameworks in Ruby.

The Format

First things first, we're going to take a quick look at Rack, which all of these frameworks (including Rails) are built on top of. After that we'll compare them using MVC as the basis of comparison, but we will also talk about their router. Finally we will quickly discuss when it might be appropriate to choose one over the other.

The Foundation: Rack

Rack provides a minimal interface between web servers that support Ruby and Ruby frameworks.

Rack has really been what has helped the Ruby community have such a large number of both webservers and frameworks. It allows them to communicate with each other in a standard way. A webserver like Puma, Unicorn, or Passenger only has to be built for just one interface: Rack. As long as the framework is also built upon Rack, it's able to work with any one of the webservers out there. All of the frameworks I'll be discussing in this article are built on top of Rack, and Rails is no different.

A minimal Rack application contains an object which responds to the call method. This method should return an array with three elements: an HTTP status code, a hash of HTTP headers, and the body of the response. Essentially what a framework does is to help you organize and manage creating responses that get converted into this format.

Here is the smallest Rack application:

# config.ru
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['Hello, Rack']] }

By running the rackup command (which will start a webserver), we can actually reach this endpoint from the browser or via curl.

rackup config.ru

Now we can reach it from curl:

-> curl 127.0.0.1:9292
Hello, Rack

Cuba

The tagline for Cuba is "Ceci n'est pas un framework," which translates to "This isn't a framework." You might ask yourself why it's even in the list, but luckily just below that phrase it says that Cuba is in fact a micro framework. That's why I have it listed first, just above Rack.

Cuba was written by Michel Martens with the goal of following a minimalist philosophy and only providing what's needed rather than some of the bloat and unused features that come along with much larger frameworks. Cuba is small, lightweight, and fast.

Router

Cuba provides a small router that allows you to define routes using its DSL.

Cuba.define do
  on root do
    res.write("Hello World!")
  end
end

This code is small but surprisingly powerful. The on method is looking for a clause that returns true. So in this case, when a request is made to /, or root, it will yield the block of code. This block of code has access to res, which is a Cuba::Response class. Here you can tell it what status code to use, which text to render, etc.

Because of how on works, we can nest routes together like so, where we group all of our GET requests together:

Cuba.define do
  on get do
    on "about" do
      res.write("About us")
    end
    on root do
      res.redirect("/about")
    end
  end
end

Model and controller

Cuba, as mentioned at the outset is a micro framework, and it has chosen not to include what might traditionally be the model or controller. If you are in need of a model for your app, you are welcome to bring in a solution from another framework, like ActiveRecord or Lotus::Model. What would be in the controller in Rails is put inside of the router directly.

View

Cuba comes with a plugin called Cuba::Render which helps you with the View layer. It allows you to use ERB templates by default but can easily be configured to support Haml, Sass, CoffeeScript, etc. by using the Tilt gem.

To take advantage of the views you can use three methods: partial, view, or render. In this example, I'll use render, which looks for an ERB file inside of the views folder and passes that as a content variable to a layout.erb file.

require "cuba"
require "cuba/render"
require "erb"
Cuba.plugin(Cuba::Render)
Cuba.define do
  on get do
    on "about" do
      render("about")
    end
  end
end

Here is our layout file (views/layout.erb):

<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
  <%= content %>
</body>
</html>

And lastly our actual view template (views/about.erb):

<h1>About Us</h1>
<h2>Welcome, friends</h2>

Sinatra

Going up the ladder of complexity a little bit, we have Sinatra. Sinatra was created by Blake Mizerany in 2007, about four years after Rails began. Sinatra is probably the second most popular Ruby framework out there; it's used by many coding bootcamps to give students their first introduction to building a Ruby app.

Router

Sinatra comes with a very capable router which is centered around two things:

  • the HTTP verb (GET, PUT, POST, etc.)

  • the path of the HTTP request.

Using these, you can match the home page by using get and "/".

The main job of one of these blocks of code is to either respond with the text (or JSON or HTML) to be rendered, or by redirecting to another page. In this example, I'm redirecting the root URL to "/hello", which renders some text.

get "/" do
  redirect "/hello"
end
get "/hello" do
  "Hello, World"
end

Model and controller

Like Cuba, Sinatra doesn't come with a model or controller layer out of the box, and you're welcome to use Active Record, Lotus::Model, or another ORM of your choosing.

View

Sinatra comes with a View layer which is built-in and allows you to use ERB, Haml, and Sass, among other templating engines. Each of these different templating engines exposes itself to you in the router via its own rendering method. To render an ERB file, we can simply say erb :hello, to render the views/hello.erb file which will be embedded into a views/layout.erb file via a yield.

get "/hello" do
  erb :hello
end

Another feature that Sinatra has is the ability to define helper methods which can be used inside of the templates. To do that, you use the helpers method and define your own methods inside of the block.

helpers do
  def title
    "Sinatra Demo App"
  end
end

In our views/layout.erb file we can access the title method we defined as a helper.

<!doctype html>
<html>
  <head>
    <title><%= title %></title>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

Padrino

Our goal with Padrino is to stay true to the core principles of Sinatra while at the same time creating a standard library of tools, helpers, and functions that will make Sinatra suitable for increasingly complex applications.

Padrino and Sinatra go hand in hand. Padrino is based on Sinatra but adds many additional tools such as having generators, tag helpers, caching, localization, mailers, etc. It takes Sinatra, which could be said is on the lighter side of what a framework is, and adds some of the things missing from making it a full-stack framework.

Router

In Padrino apps, the routes and controllers are combined in one place. Instead of having a routes file where all the routes go for the whole application (although this too is possible), the controllers essentially contain the routes. The users controller contains all routes related to a User. Anything you can do with routing in Sinatra you can do here, plus some extra features like route nesting.

Bookshelf::App.controllers :books do
  get :index do # /books
    @books = Book.all
    render 'index', layout: 'application'
  end
end

Model

Padrino doesn't have its own model layer but rather comes with support for a large number of established ORMs that either live on their own or come from other frameworks.

Padrino supports Mongoid, Active Record, minirecord, DataMapper, CouchRest, mongomatic, MongoMapper, OHM, Ripple, SEQUEL, Dynamoid. While generating an app you can choose which one you're going to use (if any), and then the model generators will help you generate models according to the ORM that you chose.

padrino g model Book title:string code:string published_on:date

View

Padrino has a view layer which is rendered from within the controller. It supports ERB, Haml, Slim, and Liquid out of the box. There isn't much to say here other than it works well and as expected. Variables can be passed to the view by setting an instance variable in the controller. There are also helpers generated for each controller which can also be used in the view.

Controller

Controllers and routes in Padrino are essentially the same thing. The controller defines the routes and decides how to handle the response: whether to find data to render or to redirect elsewhere.

hbspt.cta.load(1169977, '11903a5d-dfb4-42f2-9dea-9a60171225ca');

Lotus

Lotus is a Ruby MVC web framework comprised of many micro-libraries. It has a simple, stable API, a minimal DSL, and prioritizes the use of plain objects over magical, over-complicated classes with too much responsibility.

Lotus is a full-stack MVC framework created by Luca Guidi that began in 2013. The philosophy behind it, as mentioned in a blog post by Luca, is that its goal is simplicity, aiming to be built in a modular way, relying more on plain old Ruby objects (POROs) rather than DSLs.

Lotus is actually comprised of seven different modules (or "micro-libraries"):

  • Lotus::Model

  • Lotus::Router

  • Lotus::Utils

  • Lotus::View

  • Lotus::Controller

  • Lotus::Validations

  • Lotus::Helpers

These can be used individually or brought together in a complete full-stack framework under the Lotus gem itself.

Lotus::Router

Lotus comes with a very clean and capable router. It feels very similar to the router that comes with Rails.

To make a get request to the Index action of our Home controller, we put:

get '/', to: 'home#index'

The Lotus Router also supports RESTful resources right out of the box.

resources :books, except: [:destroy]

Because Lotus is Rack compatible, we can respond with a Proc (because it has a call method), and we can even mount an entire Sinatra application inside our routes.

get '/proc', to: ->(env) { [200, {}, ['Hello from Lotus!']] }
mount SinatraApp.new, at: '/sinatra'

Lotus::Model

Lotus::Model follows a Domain Driven Design approach (see Domain Driven Design by Eric Evans), which implements the following concepts:

  • Entity

  • Repository

  • Data Mapper

  • Adapter

  • Query

An Entity is an object that is defined by its identity. In other terms, it's the "noun" or "thing" in your app: a User, a Book, a Library, etc.

class Book
  include Lotus::Entity
  attributes :author_id, :price, :title, :code
end

A Repository is the next major object in Lotus::Model, whose job it is to mediate between an Entity and the persistence layer (PostgreSQL, MySQL, etc.). Entities aren't responsible for querying themselves or persisting themselves to the database. That's the job of the Repository, and it allows for the separation of concerns.

The Repository is where you'll put all of your queries. They are actually private methods of this object, meaning you're forced to keep them organized in one place. Controllers (or even worse, Views) no longer have intimate knowledge of how to query data.

In Active Record, which is an implementation of the Active record pattern, you might write your query like this, which could exist anywhere in your application (and is quite commonly found in Controllers):

Book.where(author_id: author.id).order(:published_at).limit(8)

But in Lotus it would live inside the repository.

class BookRepository
  include Lotus::Repository
  def self.most_recent_by_author(author, limit: 8)
    query do
      where(author_id: author.id).
        order(:published_at)
    end.limit(limit)
  end
end

The job of the Data Mapper is to map our fields in the database to attributes on our Entity.

collection :books do
  entity Book
  repository BookRepository
  attribute :id, Integer
  attribute :author_id, Integer
  attribute :title, String
  attribute :price, Integer
  attribute :code, String
end

Lotus also comes with migrations to help you manage the schema of your database. These are quite similar in Lotus as they are in Rails. You have a series of command line commands which will help you generate a new migration. Once you're done writing it, you can run the migration, get the current migration the database is on, or roll it back.

bundle exec lotus generate migration create_books
# db/migrations/20150724114442_create_books.rb
Lotus::Model.migration do
  change do
    create_table :books do
      primary_key :id
      foreign_key :author_id, :authors, on_delete: :cascade, null: false
      column :code,  String,  null: false, unique: true, size: 128
      column :title, String,  null: false
      column :price, Integer, null: false, default: 100
    end
  end
end

Lotus didn't feel the need to reinvent the wheel here and is using SEQUEL under the hood to help it with migrations and communicating with the database.

bundle exec lotus db migrate
bundle exec lotus db version # 20150724114442

Here is how you would create and persist a Book:

author = Author.new(name: "George Orwell")
author = AuthorRepository.persist(author)
book = Book.new(title: "1984", code: "abc123", author_id: author.id, price: 1000) book = BookRepository.persist(book)

Lotus::View

In Lotus, the View is an actual object which is responsible for rendering a template. This varies from Rails where the controller renders the template directly.

# web/views/books/index.rb
module Web::Views::Books
  class
    Index include Web::View
    def title
      "All the books"
    end
  end
end

Inside of the template, we are now able to call books (which was exposed to us from the Controller/Action) and title to get the page title. Lotus comes with ERB templates by default, but it supports many different rendering engines such as Haml and Slim.

<h1><%= title %></h1>
<ul>
  <% books.each do |book| %>
    <li><%= book.title %></li>
  <% end %>
</ul>

Lotus::Controller

One major difference between Lotus and Rails in the Controller layer is that each Action in Lotus is its own file and class. Another difference is that @ (instance) variables aren't exposed to the View by default. We must explicitly tell the Action which variables we want to expose.

# web/controllers/books/index.rb
module Web::Controllers::Books
  class Index
    include Web::Action
    expose :books
    def call(params)
      @books = BookRepository.all
    end
  end
end

Summary

It should be said that all of these frameworks have uses and things that differentiate them one from the other. So which is the best one? Here's an answer that you'll hate:

It depends.

It depends on the requirements of your project or in some cases it can be developer preference when all else is equal.

Here's a summary of when it might be a good idea to choose one of these frameworks over another:

  • Cuba: Very close to Rack with very low overhead. I think its best use is for small endpoints where speed is crucial or for those who want full control over their entire stack, adding additional gems and complexity as needed.

  • Sinatra: Not as close to Rack, yet still far from being a full-stack framework such as Rails or Lotus. I think it's best used when Cuba is too light, and Rails/Lotus are too heavy. It's also a great teaching tool because of its small interface.

  • Padrino: For those who have an existing Sinatra app that is becoming more complex and warranting things that come in a full-stack framework. You can start with Sinatra and graduate to Padrino if needed.

  • Lotus: A great Rails alternative with a simple and explicit architecture. For those that find themselves disagreeing with "The Rails Way," or for those that really enjoy the Domain Driven Design approach.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.