Highlights of Hanami 2.0

Thank you for your interest in Hanami 2.0! If you’ve read our release announcement and would like to learn more, here are some highlights for you!

What’s new in 2.0?

Hanami 2.0 is jam packed with goodies:

  • A new app core offering advanced code loading capabilities, allowing you to organize your code however you want while providing sensible and familiar defaults
  • An always-there dependencies mixin, helping you draw clearer connections between your app’s components
  • A blazing fast new router
  • Redesigned action classes that integrate seamlessly with your app’s business logic
  • Type-safe app settings with dotenv integration, ensuring your app has everything it needs in every environment
  • New providers for managing the lifecycle of your app’s critical components and integrations
  • New built-in slices for gradual modularisation as your app grows
  • A rewritten getting started guide to help you get going with all of the above

New app core

At the heart of every Hanami 2.0 app is an advanced code loading system. It all begins when you run hanami new and have your app defined in config/app.rb:

require "hanami"

module Bookshelf
  class App < Hanami::App
  end
end

From here you can build your app’s logic in app/ and then boot the app for running a web server. This is the usual story.

In Hanami 2.0, your app can do so much more. You can use the app object itself to load and return any individual component:

# Return an instance of the action in app/actions/home/show.rb

Hanami.app["actions.home.show"] # => #<Bookshelf::Actions::Home::Show>

You can also choose to prepare rather than fully boot your app. This loads the minimal set of files required for your app to load individual components on demand. This is how the Hanami console launches, and how your app is prepared for running unit tests.

This means your experience interacting with your app remains as snappy as possible, even as it grows to many hundreds or thousands of components. Your Hanami console will always load within milliseconds, and the fastest possible tests will keep you squarely in the development flow.

Learn more about the app container and components.

Always-there dependencies mixin

No class is an island. Any Ruby app needs to bring together behavior from multiple classes to deliver features to its users. With Hanami 2.0’s new Deps mixin, object composition is now built-in and amazingly easy to use.

module Bookshelf
  module Emails
    class DailyUpdate
      include Deps["email_client"]

      def deliver(recipient)
        email_client.send_email(to: recipient, subject: "Your daily update")
      end
    end
  end
end

You can include Deps in any class within your Hanami app. Provide the keys for the components you want as dependencies, and they’ll become automatically available to any instance methods.

Behind the scenes, the Deps mixin creates an #initialize method that expects these dependencies as arguments, then provides the matching objects from your app as default values.

This also makes isolated testing a breeze:

RSpec.describe Bookshelf::Emails::DailyUpdate do
  subject(:daily_update) {
    # (Optionally) provide a test double to isolate email delivery in tests
    described_class.new(email_client: email_client)
  }

  let(:email_client) { instance_spy(:email_client, Bookshelf::EmailClient) }

  it "delivers the email" do
    daily_update.deliver("jane@example.com")

    expect(email_client)
      .to have_received(:send_email)
      .with(hash_including(to: "jane@example.com"))
  end
end

You can specify as many dependencies as you need to the Deps mixin. With it taking pride of place at the top of each class, it will help you as quickly identify exactly how each object fits within the graph of your app’s components.

The Deps mixin makes object composition natural and low-friction, making it easy for you to break down unwieldy classes. Before long you’ll be creating more focused, reusable components, easier to test and easier to understand. Once you get started, you’ll never want to go back!

Learn more about the Deps mixin.

Blazing fast new router

Any web app needs a way to let users in, and Hanami 2.0 offers a friendly, intuitive routing DSL. You can add your routes to config/routes.rb:

module Bookshelf
  class Routes < Hanami::Routes
    root to: "home.show"

    get "/books", to: "books.index"
    get "/books/:slug", to: "books.show"
    post "/books/:slug/reviews", to: "books.comments.create"
  end
end

We’ve completely rewritten the router’s engine, with benchmarks showing it outperforms nearly all others.

Learn more about routing.

Redesigned action classes

From routes we move to actions, the classes for handling individual HTTP endpoints. In Hanami 2.0 we’ve redesigned actions to fit seamlessly with the rest of your app.

In your actions you can now include Deps like any other class, which helps in keeping your business logic separate and your actions focused on HTTP interactions only:

module Bookshelf
  module Actions
    module Books
      class Show < Bookshelf::Action
        include Deps["book_repo"]

        params do
          required(:slug).filled(:string)
        end

        def handle(request, response)
          book = book_repo.find_by_slug(request.params[:slug])

          response.body = book.to_json
        end
      end
    end
  end
end

Actions in Hanami still provide everything you need for a full-featured HTTP layer, including built-in parameter validation, Rack integration, session and cookie handling, flash messages, before/after callbacks and more.

Learn more about actions.

Type-safe app settings

For your app to do its thing, you’ll want to give it various settings: behavioral toggles, API keys, external system URLs and the like. Hanami 2.0 provides a built-in settings class along with dotenv integration for loading these settings from .env* files in local development.

You can define your settings along with type constructors in config/settings.rb:

module Bookshelf
  class Settings < Hanami::Settings
    setting :email_client_api_key, constructor: Types::String
    setting :emails_enabled, constructor: Types::Params::Bool
  end
end

Your settings are then available as a "settings" component for you to use from anywhere in your app:

# Given ENV["EMAILS_ENABLED"] = "true"
Hanami.app["settings"].emails_enabled # => true

This means you can include "settings" in a list of Deps and access your settings from any class:

module Bookshelf
  module Emails
    class DailyUpdate
      include Deps["email_client", "settings"]

      def deliver(recipient)
        return unless settings.emails_enabled
        # ...
      end
    end
  end
end

Hanami loads your settings early during the app’s boot process and will raise an error for any settings that fail to meet their type expectations, giving you early feedback and ensuring your app doesn’t boot in a potentially invalid state.

Learn more about settings.

Providers

So far we’ve seen how your classes and settings can be accessed as components in your app. But what about components that require special handling before they’re registered? For these, Hanami 2.0 introduces providers. Here’s one for the "email_client" component we used earlier, in config/providers/email_client.rb:

Hanami.app.register_provider :email_client do
  prepare do
    require "acme_email/client"
  end

  start do
    # Grab our settings component from the app (the "target" for this provider)
    settings = target["settings"]

    client = AcmeEmail::Client.new(
      api_key: settings.email_client_api_key,
      default_from: "no-reply@bookshelf.example.com"
    )

    register "email_client", client
  end
end

Every provider can use its own set of prepare/start/stop lifecycle steps, which is useful for setting up and using heavyweight components, or components that use tangible resources like database connections.

You can interact directly with these providers and their components, giving you to access to parts of your app in situations where you may only need a single component. A common example here would be running database migrations or precompiling static assets, which are tasks that can run in isolation from the rest of your app.

Learn more about providers.

Slices

As your app grows up, you may want to draw clear boundaries between its major areas of concern. For this, Hanami 2.0 introduces slices, a built-in facility for organising your code, encouraging maintainable boundaries, and creating operational flexibility.

To get started with a slice, create a directory under slices/ and then start adding your code there, using a matching Ruby namespace. It’s that simple! Here’s a class that imports books for our bookshelf app:

# slices/feeds/process_feed.rb
module Feeds
  class ProcessFeed
    def call(feed_url)
      # ...
    end
  end
end

Each slice has its own slice class that behaves just like a miniature Hanami app:

Feeds::Slice["process_feed"] # => #<Feeds::ProcessFeed>

Every slice imports some basic components from the app, like the "settings", "logger" and "inflector", and slices can also be configued to import components from each other. If we added an admin slice to our app, we could allow it to invoke feed processing by specifying an import in the slice class at config/slices/admin.rb:

module Admin
  class Slice < Hanami::Slice
    import keys: ["process_feed"], from: :feeds
  end
end

Now the feeds processor is available within the admin slice:

Admin::Slice["feeds.process_feed"] # => #<Feeds::ProcessFeed>

# or as a dep:
module Admin
  class SomeClass
    include Deps["feeds.process_feed"]
  end
end

Slices can also be mounted within the router:

module Bookshelf
  class Routes < Hanami::Routes
    slice :admin, at: "/admin" do
      get "/books" to: "books.index"
    end
  end
end

Slices can also be selectively loaded at boot time, which brings great advantages for ensuring best performance of certain production workloads. If you had a process working a Sidekiq queue for jobs from the feeds slice only, then you can set HANAMI_SLICES=feeds to load that slice only, giving you code isolation and optimal memory and boot time performance for that process.

Working with slices helps you maintain a clear understanding of the relationship between your app’s high-level concerns, just like how the Deps helps with this for individual components. Slices are the key to helping your app be just as easy to maintain at day 1,000 as it was at day 1.

Learn more about slices.

Getting started with Hanami 2.0

Hopefully by now you’re excited to give Hanami 2.0 a try! To help you on your way, we’ve completely rewritten our friendly user guides to cover all the important concepts for this new release.

A huge thank you to Andrew Croome and Seb Wilgosz for delivering these guides!

7 Likes