Hanami, Why?: Bits & Bobs

aaronmallen.me · 2026-10-03 · by Long Nguyen ·generic
A tour of Hanami's foundational abstractions, dependency injection, providers, types, and contracts, with an honest look at the rough edges of each.

Hanami, Why?: Bits & Bobs

Writing

Welcome back to my "Hanami, Why?" series. If you missed it, be sure to read Hanami, Why?: Introductions before you start this issue. It is my intent to keep these issues focused on singular subjects whenever possible. However, in today's issue we are going to cover several topics that are too small to warrant their own issue but still important to have for context. As I mentioned, Hanami has a lot of important abstractions, and today we are going to be covering what I consider "system level" abstractions. We have lots to cover, but this context will be important for a further understanding of how Hanami works.

Dependency Injection

Most of the time Ruby is pretty great at telling you what a particular file depends on via `require`. However, in frameworks like Rails everything is autoloaded for you, so there often are not any calls to `require` in the code base. Dependencies end up scattered through method bodies, and the only way to know what a class depends on is to read every line of it. Now, Hanami autoloads your code too, but it also gives you a way to declare your dependencies up front via the `Deps` module. Let us look at an example:

# app/actions/users/create.rb
module MyApp
  module Actions
    module Users
      class Create < Action
        include Deps["repos.user_repo"]

def handle(request, response) user = user_repo.create(request.params[:user]) response.redirect_to routes.path(:user, id: user.id) end end end end end

Here the dependencies for our `Users::Create` action are clearly called out at the top of the file. Notice that including `"repos.user_repo"` gives us a method named `user_repo`. By default, the method name is the last segment of the key. If you do not like that name, you can alias it like so:

class Create < Action
  include Deps[users: "repos.user_repo"]

def handle(request, response) user = users.create(request.params[:user]) # ... end end

The important part to remember is that any class can have dependencies injected into it. It does not have to be an action, an operation, or a repo. Here is a plain old Ruby object with no superclass at all:

# app/slugify.rb
module MyApp
  class Slugify
    def call(title)
      title.downcase.strip.gsub(/[^a-z0-9]+/, "-")
    end
  end
end
# app/post_publisher.rb
module MyApp
  class PostPublisher
    include Deps["repos.post_repo", "slugify"]

def call(params) post_repo.create(**params, slug: slugify.call(params[:title])) end end end

`PostPublisher` does not inherit from anything Hanami gives us. It just includes `Deps` and gets its dependencies like everything else.

The other thing I want to hit home is that the file is the key, and an instance is what gets injected. Every file in `app/` is registered in your application's container under a key built from its path. `app/slugify.rb` becomes `"slugify"`, and `app/repos/post_repo.rb` becomes `"repos.post_repo"`. If you ever want to know where a dependency comes from, all you need to do is look at its key. You should also note that we never called `Slugify.new.call`. Hanami does not just load the code for you; it hands you a ready-to-use instance of that dependency.

Rough Edges

The first thing to be aware of is that `Deps` works by writing your class's initializer for you. If you need custom logic on initialization, you will have to work around the container to get it. You can still define your own `initialize`, but you must accept the injected dependencies and pass them along to `super`:

module MyApp
  class PostPublisher
    include Deps["repos.post_repo", "slugify"]

def initialize(reserved_slugs: %w[admin new edit], **deps) @reserved_slugs = Set.new(reserved_slugs).freeze super(**deps) end end end

Forget to call `super` and your dependencies will never be set. Notice also that `reserved_slugs` needs a default. The container builds your components by calling `.new` with no arguments, so anything a component needs has to come from a default or from `Deps` itself.

That leads to the second rough edge: Hanami registers every file in `app/` as a component, whether you plan on injecting it or not. If you have a class that will never be injected, like a value object that takes arguments, you can tell Hanami to skip it with a magic comment at the top of the file:

# auto_register: false

Third, your keys are just strings. If you make a typo in `include Deps["repos.post_rep"]`, Ruby will not catch it, and you will not find out until that code actually runs. Your editor's "go to definition" will not follow a string key either (although this is being worked on). The upside is that the key tells you exactly which file to open, so you are never more than one step away from the source.

Last, by default every component in your container is memoized. That means it is instantiated once, and that same instance is injected everywhere it is asked for. Your components should be stateless. Do not stash request data in instance variables and expect it to be gone by the next request. To make things trickier, components are not memoized in the test environment, so a bug like this can sail right through your test suite and only show up in production. If you truly need a fresh instance every time, you can opt a single component out with another magic comment:

# memoize: false

Providers

Have you ever opened the initializers directory in your Rails application and observed something like this?

initializers/
├── 001_high_priority.rb
├── 002_medium_priority.rb
└── 003_low_priority.rb

I know I have, and I am willing to wager you have seen something like this too. This is because initializers in Rails are loaded in alphabetical order. I have seen firsthand that failing to load dependencies in the initialization step in the correct order can cause hard-to-pinpoint failures throughout your application. Let us look at another example. Have you ever seen anything like this?

$redis = Redis.new(...)

Now, do not get me wrong, there are certainly better ways to go about this, but ultimately the problem being solved here is that Rails does not offer you a prescribed way of handling global components. This is where Hanami's providers come into play.

Providers are a way to register components with your containers. Providers have three stages in their life cycle: `prepare`, `start` and `stop`. All of the `prepare` blocks are loaded before any of the `start` blocks are, so this is where you want to do things like require dependencies. Providers are prepared and started automagically before your application boots, much like initializers. Providers are typically found in `config/providers/` or `slices/<slice>/config/providers`. Let us look at an example of a provider and how you might use one.

Hanami.app.register_provider :redis, namespace: true do
  prepare { require "redis" }

start do register :client, Redis.new(url: slice["settings"].redis_url) end end

Now, there is actually quite a bit going on here, so let us break it down bit by bit.

1. We are registering a namespaced provider named `redis`. Because it is namespaced, every component it registers lives under the `redis` key, so the `client` component we will get to in a moment can be called anywhere in your application via `include Deps["redis.client"]`. You could forgo the namespace altogether, in which case you would call the dependency via `include Deps["client"]`. If you go that route, I would definitely name it `redis_client` instead of just `client`, but I hope you get the gist. 2. We are requiring the `redis` gem in our prepare block. This ensures the library is ready to be used by the time `start` is invoked. Remember, all of the `prepare` blocks are invoked before any `start` block is invoked. So, if you have several providers, each of them will be prepared before any of them are started. 3. In our `start` block, we are registering a component called `client` and assigning it an instance of `Redis` as a value. You now have a global component that you can inject anywhere in your application. 4. You can see we call something named `slice` and pull settings off of it. In this example, the `slice` is the Hanami application itself; however, if you were dealing with a slice's provider, the `slice` would be that slice. We will probably cover slices in depth later in this series; for now, just know they are essentially a bounded context and are first-class citizens in a Hanami application.

Providers have all sorts of useful applications and are one of the many joys of working with a Hanami application. We should also take note that a provider can actually register multiple components:

Hanami.app.register_provider :argon, namespace: true do
  prepare { require "argon2" }

start do register :hasher, Argon2::Password.method(:create) register :verifier, Argon2::Password.new(...).method(:verify_password) end end

In this example you would inject these components via `include Deps["argon.hasher", "argon.verifier"]`.

Rough Edges

The biggest rough edge here is that providers will behave differently in development and test than they do in production. In development and test, a provider's start cycle is only called when something asks for its component(s). This is to help ensure development and test environments stay fast, whereas Rails will load all of your initializers no matter what environment you are in. In production, `Hanami.boot` starts every provider upfront. So a provider with a bad setting, like a missing `REDIS_URL`, can sit quietly in development and then crash your app on boot in production.

Types and Validation

I know, I know: "ew, types". Types are actually a very useful abstraction and can be used to validate and normalize user inputs. Let us start off with an example:

module MyApp
  Types = Dry::Types()
  
  module Types
    EmailAddress = Types::String.constrained(format: URI::MailTo::EMAIL_REGEXP, max_size: 255)
    
    module Normalized
      EmailAddress = EmailAddress.constructor {  it&.downcase&.strip }
    end
  end
end
module MyApp
  module Relations
    class Users < Relation
      schema :users, infer: true do
        attribute :email, Types::Normalized::EmailAddress
      end
    end
  end
end

Alright, let us unpack this, shall we? Essentially, what we have done here is we have created `Types::EmailAddress`, which we can use to validate that any string is a valid email address. You can do that via `Types::EmailAddress["[[email protected]](https://aaronmallen.me/cdn-cgi/l/email-protection)"]`. If the email address is valid, it is returned as a string; if it is not, then an error is raised. You would want to use this in places like parameters to ensure the user is providing you valid values. Next, we have created `Types::Normalized::EmailAddress`, which carries all the same validation properties as `Types::EmailAddress`, but it will also ensure the email address is lowercase and devoid of whitespace. Last, we are telling the `Users` relation that the `email` field should be `Types::Normalized::EmailAddress`. This ensures all emails are normalized before they ever hit the database.

In Rails, `normalize` methods are typically tied to the model they do normalization in. This means anywhere you would need to normalize an email address, you are defining the same logic in multiple places. This is bad for several reasons, and you would likely want to abstract that out into something like a concern. Hanami gives you a framework that already expects this logic to be abstracted. Types are reusable and can be called throughout your Hanami application. Types can also be used to help you validate inputs as well.

Contracts

Hanami does not provide any generators for Contracts, and they are not considered first-class citizens by default in a new Hanami application. However, in my Hanami applications they are treated as such. Dry Validation is pulled in by Hanami as a dependency by default, so the pieces are already there for you. It is a simple matter of creating your base class and adding an `app/contracts/` directory to your application. In a Rails application, validation is usually tied directly to individual models. Once again, this means if you have validation logic that is used in multiple places, it is either repeated in the individual models or abstracted out into a concern. Dry Validation gives us this abstraction for free.

module MyApp
  module Contracts
    class NewUserContract < Contract
      params do
        required(:user).filled(:hash) do
          required(:email).filled(Types::EmailAddress)
          required(:password).filled(:string, min_size?: 8)
        end
      end
    end
  end
end

I personally like to do my validation at the operation layer as this ensures params are validated any time a mutation occurs. That might look something like this:

module MyApp
  module Operations
    class CreateUser < Operation
      include Deps["contracts.new_user_contract"]
      
      def call(params)
        validated = step validate_params(params)
        # ...
      end
      
      private
      
      def validate_params(params)
        result = new_user_contract.call(params)
        result.success? ? Success(result.to_h) : Failure(result.errors.to_h)
      end
    end
  end
end

I can call this contract anywhere I would like throughout my application, meaning I can deploy my validation layer anywhere it is needed. Pro tip: the `params` method in a Hanami action can take a class as an argument.

class Create < Action
  params Contracts::NewUserContract

# ... end

The last thing we need to cover about contracts is custom rules. The example above will ensure that an email is the right format, and that the password is at least eight characters long. What if I also want to validate that the email is unique? That is where rules come into play:

module MyApp
  module Contracts
    class NewUserContract < Contract
      include Deps["repos.user_repo"]
      
      params do
        required(:user).filled(:hash) do
          required(:email).filled(Types::EmailAddress)
          required(:password).filled(:string, min_size?: 8)
        end
      end
      
      rule user: :email do
        next unless user_repo.email_exist?(value)
        
        key.failure("email is already taken")
      end
    end
  end
end

Now, personally, I do not allow my contracts to depend on repos. In my mind, the validation layer should be as light as possible, and injecting a database layer into the contract sort of violates that principle. I would typically handle this check in the operation or the repo layer. However, this is one of the beautiful things about Hanami: you get to decide how to structure your application.

Rough Edges

Configuration for contracts can be a bit confusing, and I often find myself playing with my internationalization files quite a bit trying to figure out the right keys to call when I want to customize an error message. However, I know this is on the Hanakai team's radar, and it is something we are looking to simplify after adding internationalization in Hanami 3. Additionally, picking the right kind of type can be confusing. `Types::Integer` will reject the value `"42"`, but `Types::Coercible::Integer` will happily take it and return an integer to you. I do not view this as a rough edge as much as I view it as a bit of a learning curve. I can say I do not typically need to visit the dry types documentation anymore to figure out which type to use.

Alright, we have covered a substantial amount of ground today. I know we have not yet really dived into Hanami as an application framework, but I assure you that having this context will be very useful moving forward in this series. I do want to take a moment to thank you for the overwhelmingly positive reception to the introduction of this series. I do hope you found this issue helpful. I look forward to seeing you in the next issue, where we will be covering Actions, one of my favorite abstractions in Hanami.

Found a typo or want to argue about patterns? Let me know.

Also posted onBlueskyMastodon

Responses

7 likes · 7 reposts

1. lemmy.bestiver.seOct 2, 2026 2. LobstersOct 2, 2026 Hanami, Why?: Bits & Bobs by @aaronmallen https://lobste.rs/s/zcthix #rubyhttps://aaronmallen.me/writing/hanami-why-bits-bobs ruby Hanami, Why?: Bits & Bobs 3. paca-vacaOct 2, 2026 Continue, I like the series! 4. Albert OlivaOct 2, 2026 @aaronmallen @usuario Love seeing more Ruby around here! 💎 If you ever need somewhere to deploy your projects, I just launched Enkihost, hosting made for Rails, Sinatra and Jekyll. Just git push and it's live, with Postgres, Redis and SSL included. Jekyll sites are free and Rails has a 14-day trial.(It's my own project, so any feedback would mean a lot!) https://enkihost.com Enkihost | Ruby Hosting (Rails, Sinatra & Jekyll)

aaronmallen.me · 30 links

Comments (0)

No comments yet