Skip to content

Hanami

Customers and Billing are two Hanami slices, and each is imported as its own Carton. Customers registers customers. Billing imports Customers, validates invoices, calculates tax, and stores them. The framework Carton loads Hanami, Dry, and Rom once. The shop Carton exports its Rack app to Main. Source on GitHub.

This is the same workflow as the Rails example. The example uses only public Hanami, Dry, Rom, and Carton APIs, and it patches nothing.

  1. POST /customers with customer: { name: "Ada" } registers a customer and returns its ID and trimmed name.
  2. POST /invoices.json with invoice: { customer_id: 1, subtotal_cents: 4200 } issues an invoice for that customer. Billing imports Customers and uses its Directory to resolve the customer before persisting the invoice.
  3. GET /invoices/1.json returns id, customer_id, subtotal_cents, tax_cents, and total_cents; GET /invoices/1 renders HTML with the escaped customer name and total.

Both POSTs return 201. Tax is ten percent, rounded half up in integer cents: 4200 produces 420 tax and 4620 total. Stored tax is retained when the invoice is read later. Contract errors return 422 with field errors, and malformed JSON or a non-object body returns 400; an unknown customer or invoice returns 404.

Gemfile, Gemfile.lock # one locked bundle; path gems for each Carton
main.rb # repeatable Rack demo, temporary SQLite
test/ # command, HTTP, and isolation tests
cartons/
framework/
lib/framework.rb # Framework Entrypoint; owns Hanami, Dry, and Rom
lib/framework/{prepare,cli}.rb # anchored framework lifecycle and CLI
customers/
lib/customers.rb # Entrypoint, imports Framework, local Zeitwerk loader
lib/customers/config/slice.rb # Hanami slice rooted in this Carton
lib/customers/{actions,operations,contracts,repos,relations}/
billing/
lib/billing.rb # Entrypoint, imports Framework and Customers
lib/billing/config/{slice,providers/customer_directory}.rb
lib/billing/{actions,operations,contracts,repos,relations,views,templates}/
shop/
lib/shop.rb # imports and registers domains, exports Shop::App
config.ru # Rack bootstrap imports Shop
bin/hanami # CLI bootstrap imports Shop, then enters Framework
config/
boot.rb # sets up Bundler for the shop's Imports
app.rb # Hanami app configuration and JSON body parser
routes.rb # slice routes
slices/{customers,billing}.rb # import and bind the existing slice namespaces
providers/db.rb # DATABASE_URL and derived test database
db/migrate/ # shared schema, run by the Hanami CLI

Repositories pass IDs and plain Hashes between Cartons. Customers’ Directory returns a plain customer Hash, so Billing never touches Customers’ repos or relations.

The shop’s Entrypoint imports Framework and loads Shop::App from config/app.rb. Hanami finds the slices in config/slices/. Each slice file imports its Carton instead of requiring a second copy.

Each Carton loads its own code with its own Zeitwerk loader and tells Hanami’s loader to skip that source tree. Hanami still wires the components and provides the Deps mixin. When a component is resolved, the Carton’s autoload runs. Actions, operations, contracts, and repos therefore run in their domain’s Carton, in both lazy and eager modes. Slice configuration, providers, and migrations run in Framework. Setting HANAMI_EAGER_LOAD=true boots all providers and components up front, after migrating.

Each exported namespace is bound in the framework Box by its own Framework.register call. Shop registers Customers and Billing from its slice files, config/slices/customers.rb and config/slices/billing.rb, and registers itself from lib/shop.rb. Registering the same module again is allowed. Registering a different module under the same name is rejected.

Framework also keeps its deferred requires in its own Box. Without that, a require made during a nested Import could load part of Dry into the importing Carton. Main receives no Hanami, Dry, or Rom constants.

cartons/framework/lib/framework.rb
require 'carton'
# The framework's gems and singleton registries live in this Carton.
require 'hanami'
require 'hanami/db/relation'
require 'hanami/db/repo'
require 'dry/validation'
require 'dry/operation'
module Framework
Hanami = ::Hanami
Dry = ::Dry
# Hanami resolves slice namespaces here. Bind them explicitly; never copy
# their classes into Object.
def self.register(*namespaces)
namespaces.each do |namespace|
name = namespace.name
if Object.const_defined?(name, false)
unless Object.const_get(name, false).equal?(namespace)
raise ArgumentError, "namespace already registered: #{name}"
end
else
Object.const_set(name, namespace)
end
end
end
def self.cli(argv, script:)
$PROGRAM_NAME = script
ARGV.replace(argv)
Ruby::Box.current.require_relative 'framework/cli'
end
def self.prepare
# Anchor deferred framework requires in the framework Box during nested Imports.
Ruby::Box.current.require_relative 'framework/prepare'
end
end
# Export: shared Hanami and Dry modules and the registration and CLI helpers.
export Framework
cartons/customers/lib/customers.rb
require 'carton'
# Actions render JSON with to_json.
require 'json'
module Customers
# Carton boundary: Hanami and Dry come from the framework Carton's Export.
Framework = import 'framework'
Hanami = Framework::Hanami
Dry = Framework::Dry
private_constant :Framework, :Hanami, :Dry
end
require_relative 'customers/config/slice'
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir(File.expand_path('customers', __dir__), namespace: Customers)
loader.inflector.inflect('db' => 'DB')
loader.ignore(
File.expand_path('customers/config', __dir__),
File.expand_path('customers/templates', __dir__),
)
loader.setup
# Hanami keeps its container and conventions; this loader owns source evaluation.
Customers::Slice.autoloader.ignore(File.expand_path('customers', __dir__))
# Export: the slice's namespace.
export Customers
cartons/billing/lib/billing.rb
require 'carton'
# Actions render JSON with to_json.
require 'json'
module Billing
# Carton boundary: Hanami and Dry come from the framework Carton's Export.
Framework = import 'framework'
Hanami = Framework::Hanami
Dry = Framework::Dry
private_constant :Framework, :Hanami, :Dry
end
require_relative 'billing/config/slice'
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir(File.expand_path('billing', __dir__), namespace: Billing)
loader.inflector.inflect('db' => 'DB')
loader.ignore(
File.expand_path('billing/config', __dir__),
File.expand_path('billing/templates', __dir__),
)
loader.setup
# Hanami keeps its container and conventions; this loader owns source evaluation.
Billing::Slice.autoloader.ignore(File.expand_path('billing', __dir__))
Billing.private_constant :TaxPolicy
# Export: the slice's namespace.
export Billing
cartons/shop/lib/shop.rb
require 'carton'
module Shop
# Carton boundary: Hanami comes from the framework Carton's Export.
Framework = import 'framework'
Hanami = Framework::Hanami
Framework.register(self)
private_constant :Framework, :Hanami
require_relative '../config/app'
Framework.prepare
end
# Export: Main receives an ordinary Rack application; it never loads Hanami.
export Shop::App

From the repository root:

bundle exec rake example:hanami

The task installs the bundle, runs the temporary-database demo, and runs the command and HTTP tests in fresh processes. It runs them with both lazy and eager loading. For a persistent local app, install the bundle, then run:

cd examples/hanami/cartons/shop
RUBY_BOX=1 bin/hanami db migrate
RUBY_BOX=1 bin/hanami routes
RUBY_BOX=1 bin/hanami run 'puts Hanami.app.slices.keys.inspect'
RUBY_BOX=1 bin/hanami server --host=127.0.0.1 --port=2300

Development and test use separate SQLite databases under cartons/shop/db/. Set DATABASE_URL to choose another, and the test database name is derived from it. Generated databases and logs are git-ignored. Migrations create the tables; importing Shop creates none.

All migrations live in Shop’s config/db/migrate/, while the Rails example keeps them per Engine. The slices share Shop’s database, and the Hanami CLI reads one config/db/ per database: when several slices have one, it warns and uses only one.

The tests live in examples/hanami/test/ and import Shop as main.rb does. The Rails example runs its workflow tests through bin/rails test instead, since Rails has its own test runner.

Use the shop’s bin/hanami. It imports Shop before invoking the standard CLI in Framework. Verified commands are db migrate, routes, run, and server. Stock bundle exec hanami, generators, the console, asset builds, mailers, and background jobs are not covered. Restart after source edits, since no Carton reloading hooks are installed. Shared framework classes and registries are still mutable objects, as in the Rails example.