Hanami 1.3 > Hanami 2.2 : tips & notes about my journey

As a long time Hanami user (8 years) for my SaaS company, I’m currently working on the migration from 1.3 to 2.2. Thanks to the team for this amazing piece of software :hugs:

With a quite big codebase and many things that work quite well, my first step is to keep most of the things in place, while embrassing some new concepts.

I’m making good progress, and I take notes along the way. I think that they may interest some fellow migrators like me, so here it is, I’ll add stuff in this thread.

4 Likes

Use case #1: compiling scss files

I like using scss files, useful for class nesting, variables, and few other things. Hanami 1 did that out of the box IIRC, so I wanted to be able to do it in Hanami 2, while keeping the official assets pipeline

After a few search in the guide + forum + a bit of ChatGPT, here is what works for me:

  • Add the dedicated plugin for esbuild (this will update package.json)

npm install --save-dev esbuild-sass-plugin sass

  • Load the plugin for asset compilation in app/config/assets.js (or slices/your_slice/config/assets.js if you want to configure one specific slice only)
import * as assets from "hanami-assets";
import { sassPlugin } from "esbuild-sass-plugin";

await assets.run({
  esbuildOptionsFn: (args, esbuildOptions) => {
    // Modify the esbuild options to include the sass plugin
    esbuildOptions.plugins = [
      ...(esbuildOptions.plugins || []), // preserve any existing plugins
      sassPlugin() // add sass plugin
    ];
    return esbuildOptions;
  }
});
  • Import the scss file in the app/assets/js/app.js file (or slices/your_slice/assets/js/app.js if you’re in a specific slice)
import "../css/app.css";
import "../css/your_file.scss";

This part was the most counter-intuitive for me as I was handling some stylesheet files, but I understand that it’s part of the compilation pipeline

With that in place, your main app.css compiled file, that you can load in your layout with <%= stylesheet_tag "app" %> will have the content of both app.css and your_file.scss files :tada:

@timriley would it make sense to enrich the official doc with concrete example like that? just let me know

Use case #2: slice based application, only start one slice at the time

I like monoliths when it comes to code base sharing, but I prefer spawing individual services (app / api / backoffice / pdf renderer… etc) for the run (dev or prod, doesn’t matter)

With Hanami 1.3 I’ve been doing that from the beginning by using the different apps (subfolder of apps directory)

With Hanami 2, obviously the slices are made for that. But I’ve came to the point where my app is almost empty, and I want to only start individual slices.

Here is how I did that (a bit simplified for the sake of example)

# config/routes.rb
# frozen_string_literal: true

module MyApp
  class Routes < Hanami::Routes
    
    if ENV["HANAMI_SLICES"] == "api"
      slice :api, at: "/" 
    elsif ENV["HANAMI_SLICES"] == "backoffice"
      slice :backoffice, at: "/"
    end
    
  end
end
# slices/api/config/routes.rb
# frozen_string_literal: true
require_relative "../middlewares/rack_global_error_handler" # an example of a custom middleware

module API
  class Routes < Hanami::Routes

    # Here is how I inject my custom middleware in the stack
    use API::Middlewares::RackGlobalErrorHandler

    # We are interested in the body of the request
    use :body_parser, :json

    scope 'api' do
      scope 'v1' do
        get  "/status", to: "v1.utils.status", as: :utils_status # pointing to an action in my slice
      end
    end
    
  end
end

Then we need to tweak Procfile a little bit by defining specific targets

#Procfile.dev
api: HANAMI_PORT=2303 bundle exec hanami server
backoffice: HANAMI_PORT=2308 bundle exec hanami server
assets: bundle exec hanami assets watch

And finally we adjust foreman launching to adapt to the slice we want to launch:

# bin/dev
#!/usr/bin/env sh

if ! gem list foreman -i --silent; then
  echo "Installing foreman..."
  gem install foreman
fi

# API slice does not need assets watching
if [ "$HANAMI_SLICES" = "api" ]; then
  FOREMAN_ASSETS=""
else
  FOREMAN_ASSETS=",assets=1"
fi

FOREMAN_M="$HANAMI_SLICES=1$FOREMAN_ASSETS"

exec foreman start -f Procfile.dev "$@" -m $FOREMAN_M

You can then launch the api slice alone :tada:

HANAMI_SLICES=api bundle exec hanami dev

Or the backoffice slice, which will include assets watching, with:

HANAMI_SLICES=backoffice bundle exec hanami dev
1 Like

Use case #3: load boostrap icons

More generally speaking, this example could help someone trying to integrate external css files from node modules

I’ve been a long time user of bootstrap glyphicons since v3, which can be used as a font and which I find very easy to use. But they abandonned them at some point. Now they are back, in a dedicated package. So I took the time to integrate them properly in my fresh Hanami 2 project

  • First, add the module with npm i bootstrap-icons (this will update package.json)

  • Load the plugin for asset compilation in app/config/app.js (or slices/your_slice/config/app.js if you want to configure one specific slice only)

// Import boostrap icons only
import "bootstrap-icons/font/bootstrap-icons";

import "../css/app.css";

The css plugin will then be included in the built asset file app.css that should already be included in your layout thanks to the stylesheet_tag helper

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <%= favicon_tag %>
    <%= stylesheet_tag "app" %>
  </head>
[...]

The project link: https://icons.getbootstrap.com/

1 Like

That is very cool of you to document the challenges with the migration. Something I saw/heard a lot of people complain about was the lack of official guidelines when migrating from 1.3 to 2.x I think it would be really valuable to add those as some more official notes to the docs

Thank you for your feedback. I know that it is very specific to my experience, which is one way to use Hanami, but if it can help some people, it would be amazing. For the moment I just document along the way and we’ll see where we can go. But I definitely think that part of this info could go into a documentation, or a cookbook

Use case #4: use Hanami interactors

I’ve always loved this “interactor” pattern brought by Hanami / Lucas. I know it has some flaws, and I wish I could change a few things, but I have to chose my battles. And I must say that using interactors helped me build my tool the way it is now.

I have 300 of them currently in production, so my goal while migrating to Hanami 2 is to keep them like that for the moment. I’ll consider how to go forward once mi migration is done.

Now that the lib is not part of the Hanami core any more, I started by using the drop-in replacement proposed by Nerian.
Here is a way to do it:

Add the gem to your Gemfile

# Gemfile
gem 'simple-interactors'

Then launch bundle install
The class can be used with a simple require 'hanami/interactor' anywhere you want

But I wanted to be a bit wiser when using them, and tried to use the providers, in what could be seen as an exotic way, but that I love !

Here is my provider:

# config/providers/interactors.rb
Hanami.app.register_provider(:interactors) do
  prepare do
    require 'hanami/interactor'
  end

  start do
    total_loaded = 0
    Dir["./lib/myapp/interactors/*.rb"].each do |file|
      require file
      total_loaded += 1
    end

    register "interactors", true

    target["logger"].info "🟢 Interactors loaded (#{total_loaded} files) ✓"
  end
end

As they are, by nature, shared accross slices, I mark them as shared in my app class

# config/app.rb
# frozen_string_literal: true

require "hanami"

module Myapp
  class App < Hanami::App

    # Some path can be excluded from auto registration into container:
    # config.no_auto_register_paths << "structs"

    # Expose some components in every slices:
    config.shared_app_component_keys += [
      "interactors",
    ]

#[...]

I can finally load them in a slice with:

module API
  class Action < Myapp::Action

    include Deps[
      "interactors",
    ]

#[...]

I still need to instantiate them by hand when I need them, but at least they are correctly lazily loaded :tada:

Thanks for sharing this approach with the interactors, @inouire!

This is interesting for me to see. I know you’re doing everything you do to streamline the upgrade path, so I understand you have to make compromises here, but I do want to ask you a couple of questions :slight_smile:

On providers: you should only need a provider if you have to do a special one-off configuration somewhere, or you otherwise want to register useful objects in the container.

Your provider doesn’t look like it does either. And to boot, it looks like your interactors are in /lib/. Classes in /lib/ won’t be auto-registered into the app container, which for your interactors is good, because their design is not compatible with auto-registration¹. However, classes in /lib/ are auto-loaded via Zeitwerk, so you should just be able to type their class name in a file and they’ll be loaded automatically. To me, this seems like this should already be enough? What exactly was going on such that you needed this provider?


¹ Looking at the Hanami 1.x interactor design via the SimpleInteractors README, these old-style interactor objects are not designed in a way that will allow container registration. They receive their per-request arguments via #initialize, whereas our container expects auto-registered objects to be initialized via a single call to .new, no arguments. It also expects those objects to be able to be used multiple times after initialization, whereas the old-style Hanami interactors are one-time-only objects.

Thanks Tim for the hint about auto loading. Maybe I lost myself while working on the migration, so I’ll try it with Zeitwerk auto-loading and let you know how it goes!

1 Like

@timriley ok, now I remember why I did that. The interactors are copied as-they-are from my legacy project, allowing me to smoothly prepare my migration (v2), while still modifying things in production (v1.3). So the interactor classes don’t have the expected path for auto loading, unless I tweak each one of them a bit, which I don’t want to do right now.

So my “provider” (which is not a real one, I agree) is a sort of poor man’s autoloading, allowing to bootstrap my legacy code into the new one.

Leveraging the true power of the container will be one of the first projects, after I migrated (I have 4 projects to migrate, of of them being really huge, so it could take time)