Sphere Partners

Hanami: A Full-featured, Lightweight Alternative to Ruby On Rails

Date Published

Reading time

7 min
Hanami: A Full-featured, Lightweight Alternative to Ruby On Rails
In this article

Hanami is a Ruby MVC web framework made up of small, single-purpose libraries that can be used independently.1 It promotes strong architecture through the use of plain objects, as opposed to magical, overly-complicated classes. Whereas Ruby On Rails is a noisy metropolis, containing functionality you may never need or use, Hanami lets you assemble your project’s stack with an understanding of how the various parts of your application interact. While Rails follows an ease-of-use philosophy, Hanami stands for simplicity. 2

Hanami offers many advantages. It is:

  • Full-featured, supporting many of the features that Rails does, including routing, controllers/actions, models, views, migrations, validations, mailers, and assets.
  • Fast and lightweight, consuming 60% less memory than other full-featured Ruby frameworks.3
  • Secure, offering advanced features like synchronized tokens against CSRF, HTML escaping to prevent XSS, clear database API to avoid SQL injection, and browser's Content-Security-Policy.
  • Simple in design, allowing for flexibility when changing code.

Architecture

Hanami is inspired by

Clean Architecture

and

Monolith First

.4  Separating the domain logic from the delivery mechanism is one of the many characteristics of the Clean Architecture approach. Software developer and author Martin Fowler notes that Monolith First is based on the idea that “you shouldn't start a new project with micro services, even if you're sure your application will be big enough to make it worthwhile.”5  When using

Hanami, you can always extract your sub-application (your API, for instance) into a separate micro service.

A Rails application comes with an app directory along with Rails support for mountable engines, but you still may be tempted to shuffle the domain logic with a delivery mechanism. In contrast, Hanami forces you to separate the domain logic from the delivery mechanism. You can see what the Hanami application is capable of, simply by looking at its apps/ directory. There you'll see the list of sub-applications. Each one is a high-level part of your application (e.g. admin panel, API, user web interface) that functions as a delivery mechanism to the business logic that lives under lib/. Models, concepts, and other parts of the business logic live under the lib/ directory and interact to form the domain logic of your application.

Directory Structure

bash
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── apps
├── config
├── config.ru
├── db
├── lib
├── public
└── spec

However, differences start to appear when you get further into the directory:

bash
apps
├── admin
├── api
└── web

Here you can see the list of sub-applications, which are the delivery mechanisms of your application. Each sub-application may also look familiar to you:

bash
apps/web/
├── application.rb
├── assets
│ ├── favicon.ico
│ ├── images
│ ├── javascripts
│ └── stylesheets
├── config
│ └── routes.rb
├── controllers
│ └── dashboard
│ └── index.rb
├── templates
│ ├── application.html.erb
│ └── dashboard
│ └── index.html.erb
└── views
├── application_layout.rb
└── dashboard
└── index.rb

Although the Hanami directory’s structure looks similar to the Rails directory’s structure, there are some notable differences between the two:

  • Controller is not a Ruby class. It's a simple Ruby module that groups actions together.
  • View is a Ruby class, often referenced as a View Model, and templates are rendered in the context of these specific views.
  • No models directory is visible here because models are part of the domain logic of your application.

Here is the content of the last significant directory, lib, which contains the business logic of your application:

bash
lib
├── bookshelf
│ ├── entities
│ │ └── book.rb
│ ├── mailers
│ │ └── templates
│ └── repositories
│ └── book_repository.rb
└── bookshelf.rb

Routing

Routing in Hanami is implemented using the

Hanami::Router

micro library. It is a Rack-compatible, lightweight and fast HTTP router for Ruby. You will find it is similar to Rails routing in terms of resource routing, GET/POST/PUT/PATCH/DELETE/TRACE HTTP methods, redirects, and mounting Rack applications. Every action that responds to #call or .call can be specified as an endpoint. If it's a string, the router will try to instantiate a class from it:

text
router = Hanami::Router.new
router.get '/hanami', to: ->(env) { [200, {}, ['Hello from Hanami!']] }
router.get '/middleware', to: Middleware
router.get '/rack-app', to: RackApp.new
router.get '/method', to: ActionControllerSubclass.action(:new)
router.get '/hanami', to: 'rack_app' # resolves to RackApp
router.mount 'dashboard#index', at: '/dashboard' # resolves to Dashboard::Index

Controller / Actions

Whereas controller in Ruby On Rails is a class that implements actions by implementing corresponding methods, controller in Hanami is a Ruby module that groups actions together. These actions are then implemented as separate Rack-inspired classes and are the endpoints that respond to incoming HTTP requests. This approach makes actions easier to test in isolation because they are identified by their single responsibility. Therefore, your controllers don’t become bloated. Similar to Rails, you can still define your before filters. Unlike Rails, you can define the instance variables you want to expose to view, params whitelisting, coercion and validation at the action level, and you can insert action-specific middleware. The entire process looks like this:

text
# apps/web/controllers/user/update.rb
module Web::Controllers::User
class Update
include Web::Action
use MyMiddleware.new(param: :value)
use OmniAuth::Builder do
# ...
end
before :authenticate!
expose :user
params do
required(:first_name).filled(:str?)
required(:last_name).filled(:str?)
required(:email).filled?(:str?, format?: /A.+@.+z/)
required(:password).filled(:str?).confirmation
required(:terms_of_service).filled(:bool?)
required(:age).filled(:int?, included_in?: 18..99)
optional(:avatar).filled(size?: 1..(MEGABYTE * 3))
required(:address).schema do
required(:line_one).filled(:str?)
required(:state).filled(:str?)
required(:country).filled(:str?)
end
end
def initialize(repository = UserRepository.new)
@repository = repository
end
def call(params)
halt 400 unless params.valid?
@user = @repository.find(params[:id])
# ...
end
private
def authenticate!
halt 401, 'You are not authenticated' unless authenticated?
end
end
end

Views

In Hanami, views are the classes responsible for rendering templates in the context of these view classes, further differentiating it from Rails. This makes testing view models in isolation easier. Hanami view supports helpers, as does Rails. Unlike Rails, in which view helpers are the anti-pattern, Hanami lets you explicitly include helper modules, so you avoid naming conflicts and a large number of public methods appearing in the view context.

Models

A persistence layer in Hanami is available through the use of

Hanami::Model

,

but because Hanami is ORM agnostic, Hanami::Model is a soft-dependency that can be replaced by any other ORM, even ActiveRecord. Whereas in Rails, ActiveRecord is expected to be the solid layer of your domain logic, the same is not true for Hanami. Hanami::Model implements a

Repository pattern

by separating the expressed behavior

(

Entity

)

from the persistence layer (

Repository

).

Entities are the model domain objects defined by their identity, and repositories are the objects that mediate between entities and the persistence layer. This approach reduces high-coupling between domain objects and persistence and isolates persistence logic to specific objects, instead of exposing it in all layers of your application as it would with ActiveRecord.

Entities are the small objects that have only a single responsibility that fits the domain of your application. They do not relate to persistence or validations. They are like POROs that provide a thin layer over attributes:

text
class Book < Hanami::Entity
def published_days_elapsed
Date.today - published_at.to_date
end
end
book = Book.new(id: 1, published_at: Date.today)
book.id
book.published_at
book.published_days_elapsed

As mentioned above, repositories are objects that mediate between entities and the persistence layer. Basically, they group a set of database queries together:

text
class BookRepository < Hanami::Repository
def count
books.count
end
def most_recent_by_author(author, limit: 8)
books
.where(author_id: author.id)
.order(:published_at)
.limit(limit)
end
end

The

official Hanami::Model documentation page

provides

helpful information on the many benefits of repositories.6

Summary

Hanami is a fast, secure, full-featured alternative to Ruby on Rails. It allows developers to build concise, modular, extensible code that can be easily maintained and repurposed. It has a reliable, strong architecture that results in a reliable, strong application. It is not suitable for rapid prototyping, unlike Rails, and the Hanami framework requires some getting used to; however, as Hanami creator Luca Guidi reminds us, “without change, there is no challenge and without challenge there is no growth.”7

Developers get used to Rails because it seems simple, but it really is not. Its complexity is hidden under its convenient interfaces. Monkey-patching, relying on global mutable state, and complex ORM are all problems that Hanami tries to solve. In stark contrast to Rails, Hanami’s architecture is based on an approach that sounds similar to the

Unix philosophy

. Like Hanami, this method “

emphasizes building simple, short, clear, modular, and extensible code that can be easily maintained and repurposed by developers other than its creators. The Unix philosophy favors composability as opposed to monolithic design.”8

If you like using Hanami, I would encourage you to also try

Trailblazer

, a High-Level Architecture For The Web.9 It is framework-agnostic, and you can use it with your Hanami or Rails application. You will find many useful and convenient features, along with good architecture. It's even easy to start refactoring your legacy application with Trailblazer.

Endnotes

More to read

Exploring the Integration of AI in Software Development: A Full-Stack Developer's Perspective
Software Development,  Data & AI,  ChatGPT

Dive into Sphere's full-stack developer journey with AI – from tackling code with GitHub Copilot to unleashing problem-solving insights with ChatGPT. Explore the potential of AI in software development projects: which tools are truly handy, how many hours can you save, and what's the next big thing? Pavel Korchak shares his insights.

Let'sConnect

Trusted by

WIZCOAutomation AnywhereAppianUiPath
Luke Suneja

Flexible, fast, and focused — let's solve your tech challenges together.

Luke Suneja

Client Partner

Loading form…