DEV Community

loading...
Cover image for Using Hanami after a decade building Rails apps

Using Hanami after a decade building Rails apps

mculp profile image Matt Culpepper Updated on ・5 min read

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:

  1. I had some extra free time and wanted to learn something new
  2. My Rails apps have lately been trending towards the use of lots of small POROs and service objects
  3. 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.

MSDH table

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.

Screenshot of my project

I also stored the table each day so there is also a historical view of the data.

Screenshot of county page

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Discussion (1)

pic
Editor guide
Collapse
andyobtiva profile image
Andy Maleh

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.