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.index → MyApp::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 manualpermit/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.