Matthew Lang avatar

Hanami

Hanami is a full-stack Ruby framework (2.x is a ground-up rewrite from 1.x) built on the dry-rb ecosystem. It favors explicit dependency injection, immutable objects, and clear boundaries over Rails-style “magic” and shared mutable global state (ActiveRecord models, ApplicationController, etc).

Directory structure

config/
  app.rb          # top-level app config
  routes.rb
app/
  actions/        # like controllers, one class per action
  views/          # presentation logic
  templates/      # ERB templates
  relations/      # ROM relations (tables)
  repositories/   # persistence, query objects
  structs/        # domain entities
lib/
  <app_name>/     # plain Ruby domain code, framework-agnostic
slices/           # optional bounded contexts, each with its own app/lib

A slice is a self-contained sub-application (e.g. slices/admin/) with its own actions, views, and container — useful for splitting a large app into bounded contexts without separate services.

Routing

# config/routes.rb
module MyApp
  class Routes < Hanami::Routes
    root { "Hello" }
    get "/posts", to: "posts.index"
    get "/posts/:id", to: "posts.show"
    resources :comments
  end
end

Routes map to action classes by name, resolved through the app’s container (posts.indexMyApp::Actions::Posts::Index).

Actions (instead of controllers)

module MyApp
  module Actions
    module Posts
      class Show < MyApp::Action
        def handle(request, response)
          response.render(view, post: post_repo.find(request.params[:id]))
        end
      end
    end
  end
end
  • One class per action, not one controller with many action methods — keeps each request handler small and independently testable.
  • Params are validated via a contract (dry-validation) rather than manual permit/require.

Views & templates

Views are separate objects from templates — a view assembles data (via exposures) and hands it to a template purely for rendering:

module MyApp
  module Views
    module Posts
      class Show < MyApp::View
        expose :post
      end
    end
  end
end
<!-- app/templates/posts/show.html.erb -->
<h1><%= post.title %></h1>

No instance variables leaking from controller to view — exposures are explicit and testable in isolation.

Persistence (ROM, not ActiveRecord)

Hanami uses ROM (Ruby Object Mapper) via hanami-router/hanami-model. Three layers:

  • Relations — thin wrappers over a dataset (a table): app/relations/posts.rb.
  • Repositories — the query API application code actually calls: PostRepo.new.find(id).
  • Structs/Entities — plain, immutable result objects, not ActiveRecord-style mutable models with callbacks.
class PostRepo < MyApp::DB::Repo
  def published = posts.where(published: true).to_a
end

No callbacks, no validations baked into the model layer — validation happens earlier, in an action’s contract.

Dependency injection (dry-system)

Hanami wires dependencies explicitly rather than relying on global constants:

class Show < MyApp::Action
  include Deps["repositories.post_repo"]

  def handle(request, response)
    response.render(view, post: post_repo.find(request.params[:id]))
  end
end

This makes swapping implementations (e.g. in tests) a matter of injecting a different object, no mocking framework required.

CLI

Command Purpose
hanami new app_name Scaffold a new app
hanami server Start dev server
hanami console REPL with app loaded
hanami generate action posts.index Generate an action
hanami db create / hanami db migrate Database tasks

Testing

RSpec is the de facto default. Actions, views, and repositories are all plain objects with injected dependencies, so most tests need no Rails-style request/integration harness — plain unit tests reach a long way.

Rails vs Hanami, in short

  Rails Hanami
Philosophy convention, “magic,” fast to start explicit, boundaries, DI
Persistence ActiveRecord (Active Record pattern) ROM (Data Mapper–ish)
Controllers one class, many actions one class per action
Views ERB + helpers + instance vars separate view objects + exposures
Ecosystem huge smaller, but dry-rb is solid

Reach for Hanami when you want Rails-level productivity but with stricter boundaries and less implicit global state — and you’re willing to trade ecosystem size for that.