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.
Business workflow
Section titled “Business workflow”POST /customerswithcustomer: { name: "Ada" }registers a customer and returns its ID and trimmed name.POST /invoices.jsonwithinvoice: { 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.GET /invoices/1.jsonreturnsid,customer_id,subtotal_cents,tax_cents, andtotal_cents;GET /invoices/1renders 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.
Layout
Section titled “Layout”Gemfile, Gemfile.lock # one locked bundle; path gems for each Cartonmain.rb # repeatable Rack demo, temporary SQLitetest/ # command, HTTP, and isolation testscartons/ 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 CLIRepositories pass IDs and plain Hashes between Cartons. Customers’ Directory returns a plain customer Hash, so Billing never touches Customers’ repos or relations.
How it fits together
Section titled “How it fits together”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.
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' endend
# Export: shared Hanami and Dry modules and the registration and CLI helpers.export Frameworkrequire '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, :Dryend
require_relative 'customers/config/slice'require 'zeitwerk'
loader = Zeitwerk::Loader.newloader.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 Customersrequire '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, :Dryend
require_relative 'billing/config/slice'require 'zeitwerk'
loader = Zeitwerk::Loader.newloader.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 Billingrequire '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.prepareend
# Export: Main receives an ordinary Rack application; it never loads Hanami.export Shop::AppFrom the repository root:
bundle exec rake example:hanamiThe 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/shopRUBY_BOX=1 bin/hanami db migrateRUBY_BOX=1 bin/hanami routesRUBY_BOX=1 bin/hanami run 'puts Hanami.app.slices.keys.inspect'RUBY_BOX=1 bin/hanami server --host=127.0.0.1 --port=2300Development 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.
CLI and limits
Section titled “CLI and limits”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.
