Hanami 2.0
Hanami 2.0 was released in November 2022. It was the perfect timing for me, since at the time, I was planning to build a small side project. I’ve never tried Hanami 1.x but read a lot about it, so when 2.0 was released I decided to give it a try. It was such an amazing journey that I decided to share my opinion about the brand-new framework.
About the project
To provide some context let’s talk a bit about my project itself.
We use tons of open source gems everyday. Thanks to the hard work of their authors, we can build our apps faster. I believe that we owe them some help. One of the ways to repay our debt, is to contribute to their software. That’s where I had the idea for ShinyGems - a place, where all issues with “help wanted” tag, for the most popular gems, are gathered together. The app was pretty straightforward to build and consists of two parts:
- background processing - periodically executed workers to pull data from RubyGems and GitHub
- web - pretty simple UI to display data we already have
The app is already live, you can see it here: shinygems.dev.
Source code is available on GitHub: kukicola/shiny_gems
First impressions
Hanami handles a lot of things differently than Rails. The first thing that catches your eye is widely used dependency container. In my opinion, dependency injection is an underrated pattern in the Ruby world (if you would like to learn more about DI, check out my article). Thanks to Hanami’s solution, we have clear, loosely coupled dependencies.
Another thing, that may be surprising at first, is that you won’t see any controllers.
In Hanami, each action is a separate class with a handle
method, which takes request
and response
arguments.
Out of the box we have parameter validation with type coercion.
At first, I was afraid that it’ll lead me to a huge number of almost-empty classes but after creating few endpoints I started to notice the value of this approach.
Each action is extremely clean - we have dependencies defined at the top, params validation in the middle and finally the handle
method fully focused on endpoints logic.
This approach solves one of the Rails problems - huge controllers with multiple actions and private methods, often used by only some of the endpoints.
Example:
module Web
module Actions
module Gems
class Index < Web::Action
include Deps["repositories.gems_repository"]
before :validate_params!
DEFAULT_PARAMS = {
page: 1,
sort_by: "downloads"
}.freeze
SORTING_DIRECTIONS = ["name", "stars", "downloads", "issues_count", "recent_issues"].freeze
params do
optional(:page).filled(:integer)
optional(:sort_by).filled(:string, included_in?: SORTING_DIRECTIONS)
end
def handle(request, response)
params = DEFAULT_PARAMS.merge(request.params.to_h)
result = gems_repository.index(page: params[:page], order: params[:sort_by])
response[:gems] = result.to_a
response[:pager] = result.pager
response[:sort_by] = params[:sort_by]
end
end
end
end
end
Views are not included in 2.0 (will be included in 2.1) but I decided to use hanami/view directly from the main branch. Since some concepts may change before the release I won’t dive deep into it. Let me just mention my two favorite things:
- View is a class, not just a template. There is a place for view-related logic.
- Automatic object decoration (using classes called
parts
)
I honestly can’t wait for the release.
ROM
Persistent layer isn’t included in 2.0 as well but you can find instructions on how to integrate rom-rb with hanami in getting started guide (it’ll be built-in in 2.1). As you can expect, it’s also different than Active Record.
The problem with models in Rails is that they tend to quickly become “master” objects containing huge amount of responsibilities. Especially, they are responsible for both persistence and business logic. ROM separates those layers. You’ll use repositories (on top of relations, mappers and commands) to communicate with the DB. Instead of models, you’ll have entities - pure data in form of hash, struct or any custom object.
How many times have you seen long chains of Active Record methods scattered across controllers, services, etc.? With repositories, you can extract it and fully focus on your business logic by working on plain data. What is more, if we combine it with dependency injection, it’s easy to test things like services or actions without touching the database.
Sample repository:
module Processing
module Repositories
class GemsRepository < ROM::Repository[:gems]
include Deps[container: "persistence.rom"]
commands :create, update: :by_pk, delete: :by_pk
auto_struct true
def by_id(id, with: nil)
query = gems.by_pk(id)
query = query.combine(with) if with
query.one
end
def pluck_ids_for_hour(hour)
gems.where(Sequel.lit("id % 24 = ?", hour)).pluck(:id)
end
def pluck_name_by_list(items)
gems.where(name: items).pluck(:name)
end
def replace_repo(old_id, new_id)
gems.where(repo_id: old_id).update(repo_id: new_id)
end
end
end
end
ROM is a really huge topic, so if you’d like to learn more, take a look at official website.
I have to admit I had a lot of problems with rom-factory (it’s like factory_bot for ROM) but I hope it’ll get better in the future.
Slices
It’s time for my favorite part - Slices. They allow you to split your application into smaller parts/modules.
For example, you can split it by features and keep the directory structure based on them, not class type
(feature/[actions/services/etc]/some_class.rb
instead of [actions/services/etc]/feature/some_class.rb
).
The closest Rails solution, that I could compare it to, would be engines.
Each slice has a separate dependency container, you can define which classes should be exposed outside the slice. When used properly they can help you keep your code organized and isolated.
What is more, you can define which slices should be loaded using an environment variable.
In ShinyGems, I have two slices - “web” and “processing”.
“Processing” is loaded in sidekiq process. It contains all the workers and gems required to pull data from APIs.
“Web”, as the name suggests, is loaded in web process. It contains all my actions and views.
I don’t need workers or gems like octokit
here so, thanks to slices, they are not loaded, keeping the memory usage low.
Directory tree of ShinyGems slices:
slices
├── processing
│ ├── config
│ ├── repositories
│ ├── services
│ └── workers
└── web
├── actions
├── assets
├── lib
├── repositories
├── services
├── templates
└── views
Compared to Rails
I hope you don’t expect me to answer the question if Hanami is better than Rails. As always - it depends. It’s different. You won’t be able to build apps in Hanami as fast as in Rails. Although, Hanami encourages you to write cleaner and easier-to-test code, which will be painless to maintain.
A breath of fresh air
In Hanami guides, you can see that is built for maintainable, secure, faster and testable Ruby applications. Looks like the authors did their job perfectly:
- maintainable - slices, no fat models or controllers - check!
- secure - CSRF protection, CSP, parameters validations etc. out of the box - check!
- faster - it’s blazingly fast (for example, ShinyGems homepage response takes 10-15ms) - check!
- testable - separation between DB and application layers, DI - check!
I really enjoyed building ShinyGems with Hanami. Even with some hiccups, I felt like I’m learning something new every day. I would recommend you to try it even if you’re strongly attached to Rails. It’s always worth to see different approaches, gain fresh perspective.
If you’ll like to take a look at the real-world Hanami app I remind you that ShinyGems is open source: kukicola/shiny_gems.