Non-Rails Frameworks in Ruby: Cuba, Sinatra, Padrino, Lotus
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.
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):
<html> <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> <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.
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.
Reference: | Non-Rails Frameworks in Ruby: Cuba, Sinatra, Padrino, Lotus from our WCG partner Florian Motlik at the Codeship Blog blog. |