I remember exactly where I was when I first watched DHH's Blog in 15 Minutes video. At the time, I was mostly using PHP for my projects, and what I saw blew my mind. 🤯
I immediately became fascinated with Ruby on Rails. I spent the next couple years of my life working a Java job while learning Ruby on Rails on the side. Eventually, I learned enough to make the career leap from Java to Rails and I spent the subsequent 10 years working on Rails apps.
Recently, I had an idea for a side project, and I decided to build it with Hanami instead of Rails. I chose Hanami for a few reasons:
- I had some extra free time and wanted to learn something new
- My Rails apps have lately been trending towards the use of lots of small POROs and service objects
- The architecture of a Hanami app feels like a natural progression from Rails
In this article, I'm going to show you some of the things I learned and really liked about Hanami.
The Project
The State of Mississippi provides a daily table with the latest counts of total COVID cases and deaths in each county.
Since the table only shows the total cases and deaths, you don't get a picture into the daily increase in cases and deaths for each county.
I decided to scrape the table each day and subtract yesterday's totals from today's totals in order to get the daily numbers.
I also stored the table each day so there is also a historical view of the data.
I stored the county name in a counties
table and I had a second table called county_updates
that looked like this:
cases
deaths
ltc_cases
ltc_deaths
county_id
-
previous_update_id
which is a self-referential foreign key
Implementation
Repositories and Entities
Hanami splits your typical Rails model into a repository class and an entity class. A repository class is where your database queries live and an entity class is a representation of your data. The entity class has attributes auto-mapped from your table schema.
I added a CountyUpdate
entity that added a few calculation methods on top of the auto-mapped attributes.
class CountyUpdate < Hanami::Entity
def new_cases
return unless previous_update
cases - previous_update.cases
end
def new_deaths
return unless previous_update
deaths - previous_update.deaths
end
def new_ltc_cases
return unless previous_update
ltc_cases - previous_update.ltc_cases
end
def new_ltc_deaths
return unless previous_update
ltc_deaths - previous_update.ltc_deaths
end
def new_cases_percent_change
return unless previous_update
(new_cases.to_f / previous_update.cases.abs * 100).round(1)
end
end
One advantage of this approach is that your queries are isolated and easily testable.
RSpec.describe CountyUpdateRepository, type: :repository do
let(:repo) { CountyUpdateRepository.new }
describe '#find_latest_by_county_id' do
it 'finds the latest update for a county' do
build_stubbed(:county, :with_updates)
latest_date = repo.find_latest_by_county_id(1).date.to_date
expect(latest_date).to eq Date.today
end
end
end
Controller Layer
In Hanami, each route corresponds to an Action class. This contrasts with Rails, where each route corresponds to a method within a Controller class.
In my app, I have a route that lists all of Mississippi's counties.
get '/counties', to: 'counties#index'
This class is invoked when the /counties
route is hit.
module Web
module Controllers
module Counties
class Index
include Web::Action
expose :counties
def call(params)
@counties = CountyWithLatestUpdateRepository.new.all
end
end
end
end
end
Likewise, for the show
route, there's a corresponding Show
action.
module Web
module Controllers
module Counties
class Show
include Web::Action
expose :county
def call(params)
@county = CountyRepository.new.find_by_name_with_updates(params[:name])
end
end
end
end
end
I really, really like this approach.
Rails controllers are often cluttered and contain extraneous code that is relevant to only certain actions. For example, why is the code to permit params
for the create
action in the same class as the index
action?
The Hanami approach adheres to the Single Responsibility Principle. You can be assured that any code in an action class is only relevant to that endpoint. It makes your actions dead simple to test.
Apps
Another one of my favorite things about Hanami is that you can have multiple apps that use your business logic and present it in different ways. Your business logic lives in the lib
directory and you can access this logic from multiple apps within the app
directory. When you create a new Hanami application, you get a server-side rendering app that lives in app/web
.
I didn't fully understand how awesome apps are until I actually needed to create a second app. After a short exchange with someone on Twitter, I offered to make the data for this project available via API.
I generated a new app in app/api
using the hanami
generator:
bundle exec hanami generate app api
Then I simply added a controller that rendered the JSON.
module Api
module Controllers
module Counties
class Show
include Api::Action
def call(params)
county = CountyRepository.new.find_by_name_with_updates(params[:name])
self.body = JSON.dump(county.to_h)
self.format = :json
end
end
end
end
end
I had a functional API in minutes.
Unlike Rails, the API was purely additive code. I didn't have to touch any existing code to handle different content types. I didn't have to write any respond_to
blocks. If I wanted to, I could've used two distinct queries for my JSON and HTML responses without branching logic.
Takeaways
My biggest takeaway from Hanami is that I felt like my code quality was improved over one of my typical Rails projects.
I've been using Rails for over a decade and sometimes I still struggle to figure out where I should put code. "Does this code belong in a model? At what point should I move model code to a service object?" I encountered this exact scenario on a Rails project recently. I moved a method back and forth between a model and PORO several times before making up my mind, and still wasn't happy with where it ended up. I eventually decided to cut my losses and just left it where it was.
I have not encountered such an issue in Hanami. It almost feels like the framework enforces code quality. It definitely feels like it makes it harder to write bad code.
There's not a whole lot of magic going on. The framework is there to guide you, not surprise you.
I was really impressed with the way this project turned out. There was a bit of a learning curve in some cases, but I was able to get over the hump. Hanami 2.0 is in the works and I'm excited to see what it brings.
P.S.
If you're interested in checking out the app, it's running on heroku @ https://ms-covid-tracker.herokuapp.com/
Code is on github
I'm on twitter @_mculp
Top comments (1)
Hanami certainly looks more domain driven and proper object oriented than Rails. I just don't think it's worth the jump though given how proliferate Rails is. This reminds me of the Rails vs Merb riverlary back in the old days. I was about to make the jump to Merb and next thing you know, they merged. In the same way, I think it would be better to bring Hanami's patterns to Rails unless Hanami decides to push things further outside the box like bake drb usage in for the web as RMI (RPC). I'll have to wait and see I suppose.
Godspeed.