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

20 Aug 2016

 

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

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

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

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:

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:

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:

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:

# 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:

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:

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