This article introduces Hanami–formerly known as Lotus–and compares its features with regular and component based Rails. I will assume you are familiar with the Ruby on Rails conventions.
Hanami is a modular rack compliant Ruby web framework promoting applications incremental design and separation of concerns beyond classical MVC.
The conventional MVC Rails structure fits simpler use cases and when their complexity increases you can use ingenuity in the form of component based Rails to achieve application modularity sometime as a stepping stone to service oriented architecture.
Preamble
Using Hanami for rapid prototyping would be a mistake–Ruby on Rails is a much better fit for that leveraging its vast pool of plugins.
Many Rails projects I saw had a plugin based prototype foundation making long term development a daunting plugin customization.
Usually at the foundation of larger Rails plugins there are framework agnostic Ruby gems and rack middlewares–like Warden at the core of Devise–that can be used to accelerate development in Hanami.
Multi application approach
Hanami helps understanding what the whole project does by supporting sub applications.
An example could be a memory game project where users can play games created by a team of game designers and with an API to publish leaderboards to mobile devices. This project domain can be broken up in to three applications: a gamezone
application where users play games, a workshop
application where game designers create games and an api
application to publish game statistics to mobile devices. The assumption is they all share some domain logic and database connection so breaking them up in to three projects at the start will create more development overhead then benefits.
Hanami
Hanami can serve such a project with its default container architecture. When the time comes for a portion to be deployed and developed in isolation it supports single applications with its application architecture.
Having all the applications as one codebase speeds up project development and it’s great that Hanami architecture supports that at its core.
Some people might quickly dismiss this as impossible in Ruby on Rails but that’s wrong.
Rails
It’s true that in a conventional Rails project this is impossible to achieve–all your code lives inside /app
and there is no way to set more applications boundaries. In simple cases this is not a problem–you could have a gamezone controller a workshop controller and an api controller but that’s rarely the case and even when it starts like that requirements will change and expand.
So for larger more complex domains–or for applications started small and grew over time–missing the boundaries makes it hard to understand what the whole project does and to migrate one of those portions to its own service.
To solve this problem a few years ago people started leveraging Ruby gems and Rails engines in what is called component based Rails architecture.
A high level component can simulate a Hanami application boundary in Rails–to find out more about this read a component based primer.
Hanami directory structure
Let’s generate our memory_game
project in Hanami–the default application would be called web
but can be overridden when first running the generator:
hanami new memory_game --application=game_zone
The directory structure at this point is:
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── apps
├── config
├── config.ru
├── db
├── lib
└── spec
Let’s add a workshop
application to allow our staff to create games leaving game_zone
focused on the web gaming experience:
hanami generate app workshop
the apps have the following directory structure:
├── application.rb
├── config
├── controllers
├── public
├── templates
└── views
All the Hanami applications share some domain logic and persistence–more about that later.
Application routing
The Hanami applications routes are mounted via config/environment.rb
:
require 'rubygems'
require 'bundler/setup'
require 'hanami/setup'
require_relative '../lib/memory_game'
require_relative '../apps/game_zone/application'
Hanami::Container.configure do
mount GameZone::Application, at: '/'
end
Mounting multiple Hanami applications on the same segment doesn’t work like mounting engines in Rails–only the first one will be served.
Hanami::Container.configure do
mount GameZone::Application, at: '/'
mount Workshop::Application, at: '/'
end
In the example above the routes provided by Workshop
will not be found. This means the mounted routes must be unique segments and you must leave the greedier one at the end, for example:
Hanami::Container.configure do
mount Workshop::Application, at: '/workshop'
mount GameZone::Application, at: '/'
end
The routing syntax is reminishent of Rails and you will find a dose–not an overdose–of magic in Hanami.
# apps/APPLICATION_NAME/config/routes.rb
get '/', to: "games#index" # => will route to GameZone::Controllers::Games::Index
to avoid the longer:
get '/', to: GameZone::Controllers::Games::Index
Controller actions
In Hanami the controller actions are classes as opposed to Ruby on Rails class instance methods. This helps defining their single responsability and prevent bloated code sharing too much logic–in Hanami you can share controller action logic with a prepare
block and callbacks.
module GameZone::Controllers::Games
class Index
include Hanami::Action
expose :games
def call(params)
@games = Game.all
end
end
end
Controller action instance variables will not be handed to the view template unless exposed–the params
and errors
variables are exposed by default. In the example above @games
wouldn’t be available to the view without the expose :games
.
Domain logic and persistence layer
Hanami is ORM agnostic but it provides hanami models a structured persistence framework with entities and repositories ideal in more complex domains where using ActiveRecord would be unsuitable–but Hanami won’t get in your way if that’s what you need. Just remember the “When to use” paragraph in Patterns of Enterprise Application Architecture:
Active Record is a good choice for domain logic that isn’t too complex, such as creates, reads, updates, and deletes. Derivations and validations based on a single record work well in this structure.
For that reason Hanami models delegate validation at the controller action level–you can read the full explanation here.
You won’t find /models
inside the /apps
directory–the application generator will build a properly namespaced structure in /lib
and since Hanami does not have autoloading it must be required from config/environment.rb
:
require_relative '../lib/memory_game'
Keep in mind the generators will add default require statements.
Adding more domain logic with gems
With Ruby namespaces alone you have some modularity but you can’t enforce a dependency structure and in complex domains your classes will end up creating a tangle of dependencies hard to follow. You can use ruby gems to handle your dependency structure.
In this example I assume my application domain has significant logic related to calculating level progression using a statistical model so inside a components
directory in the repository root I create two gems: level_progression
, bayesian_model
. The first depends on the latter and it will resolve that dependency internally without concerning the rest of the application. I only need to add the local gem level_progression
to the Hanami application Gemfile
:
path 'components' do
gem 'level_progression'
end
And require 'level_progression'
in config/environment.rb
. Now my actions can use:
calculator = LevelProgression::Calculator.new(last_score, current_score, bonus)
calculator.next_level
This same approach can be used in Rails and is a trait of component based Rails architecture.
Views and templates
In Hanami the separation between a controller action and its view layer is also well defined. The framework provides a model-view class linked to the controller action class that can act as a presenter in simple domains or instantiate more presenters in complex cases.
# apps/web/views/games/index.rb
module GameZone::Views::Games
include GameZone::View
class Index
end
end
The view will only have access to variables explicitly exposed from the controller action. There is no call to render–the framework takes care of passing from controller action to the apropriate view.
A GameZone::Views::Games::Index
view class will expect a template templates/games/index.[format].[engine]
. What Rails calls views are called templates in Hanami and support popular Ruby templating like erb, haml, slim and more.
View helpers
In Rails you have a plethora of helpers available in your views. Hanami has a vast list of helpers added trough the optional Hanami::Helpers
module–all are private so you must define an explicit interface in your view.
For example using format_number
directly in a template like:
<%= format_number game_session.current_score %>
will raise NoMethodError: undefined method 'format_number'
. Instead it must be wrapped in a current_score
helper method in the view like this:
# apps/game_zone/views/game_sessions/show.rb
module GameZone::Views::GameSessions
include GameZone::View
class Show
def current_score
# I am assuming the GameZone::Controllers::GameSessions::Show
# to expose game_session
format_number game_session.current_score
end
end
end
Then called in the template with:
<%= current_score %>
Hanami counterpart to Rails’s custom helpers is based on plain Ruby and well explained in its dedicated guides section. If you have larger more complex custom helpers you can still adopt the intention revealing helpers strategy.
Final thoughts
Hanami isn’t an overnight hack–it’s been under development for over one year, its semantic versioned API is stable, people are using it in production and you can see passion and attention to detail in its modular design, automated tests, and documentation.
Now you might wonder who’s the winner? Hanami and regular Rails excel in different areas knowing what your application needs is critical.
If are working on a production Rails application before thinking to migrate it to Hanami evaluate component based architecture.
Before committing to Hanami check for outstanding github issues some might be blocking you as well as missing features your might got used to from Rails. Luca and the team are helpful and inclusive if you want to contribute on fixing outstanding bugs–if after reading the Hanami documentation you have questions the chatroom is a fast way to get feedback.
I hope the article has sparkled interest in what Hanami has to offer. I only scratched the surface and skipped topics like testing, caching, migrations, sessions all covered in detail in Hanami excellent guides.
I am looking forward to hear other Ruby and Rails developers feedback on Hanami.