I’ve asked Gemini to summarize Hanami 2.2 Guides, so I will share with you:
Hanami v2.2 Cheat Sheet
Getting Started & Core Concepts
Initial Setup
- Install Hanami:
gem install hanami
- Create new app:
hanami new my_app_name
- Specify database:
hanami new my_app --database=postgres
(options:sqlite
(default),postgres
,mysql
) - Skip database:
hanami new my_app --skip-db
- Specify database:
- Start development server:
cd my_app_name && bundle exec hanami dev
(runs server & assets watcher viaProcfile.dev
)
App & Slices
- App (
app/
): Main application code. - Slices (
slices/
): Modular parts of your application (e.g.,admin
,api
).- Generate slice:
bundle exec hanami generate slice <slice_name>
- Slices have their own containers, actions, views, settings, providers.
- Configure in
config/slices/slice_name.rb
. - Load specific slices:
HANAMI_SLICES=api,admin bundle exec ...
orconfig.slices = [...]
inconfig/app.rb
.
- Generate slice:
Booting
Hanami.prepare
: Loads app, components are lazy-loaded. (Default for console, tests).Hanami.boot
: Loads app, providers started, components eagerly loaded. (Default for server).
Container & Components
- Classes in
app/
andslices/<name>/
are auto-registered as components. - Key Naming:
Namespace::MyClass
→"namespace.my_class"
. - Accessing:
Hanami.app["component.key"]
orMySlice::Slice["component.key"]
. - Dependency Injection (Deps Mixin):
# In a component (action, view, operation, etc.) include Deps["repos.book_repo", mailer: "utils.my_mailer"] # mailer is aliased def call book_repo.find(1) mailer.send_email(...) end
- Opt-out of registration:
# auto_register: false
comment at top of file.config.no_auto_register_paths << "my_utility_classes_dir"
- Place in
lib/<app_name>/
(still autoloaded but not registered).
- Standard Components:
"settings"
,"logger"
,"inflector"
,"routes"
.
Providers (config/providers/
)
- For components needing complex setup or lifecycle management (e.g., external API clients, database connections).
- Structure:
# config/providers/my_service.rb Hanami.app.register_provider(:my_service) do prepare do require "my_gem_or_lib_file" end start do service = MyService::Client.new(api_key: target["settings"].api_key) register "my_service.client", service # Registers the component end stop do # Teardown logic, e.g., target["my_service.client"].disconnect end end
target
gives access to the container (e.g.,target["settings"]
).
Settings (config/settings.rb
)
- Define app-specific configurations sourced from environment variables.
# config/settings.rb module Bookshelf class Settings < Hanami::Settings setting :database_url, constructor: Types::String setting :api_key, constructor: Types::String.optional setting :feature_flag, default: false, constructor: Types::Params::Bool setting :retry_attempts, default: 3, constructor: Types::Params::Integer.constrained(gt: 0) end end
- Uses
dotenv
for.env
files in development/test. - Access:
Hanami.app["settings"].my_setting
,include Deps["settings"]
,target["settings"]
.
Autoloading (Zeitwerk)
- File paths map to constant paths (e.g.,
app/services/user_creator.rb
→Bookshelf::Services::UserCreator
). app/
andlib/<app_name>/
are autoloaded.- Other
lib/
dirs, gems, and someconfig/
files need explicitrequire
. - Inflections:
config.inflections { |i| i.acronym "API" }
.
Environments
HANAMI_ENV
(orRACK_ENV
). Defaults to:development
.Hanami.env
(returns symbol),Hanami.env?(:production)
.- Env-specific config:
environment(:production) { config.logger.level = :info }
.
Request/Response Cycle
Routing (config/routes.rb
)
- Define routes:
module Bookshelf class Routes < Hanami::Routes # HTTP_METHOD "/path/:variable", to: "action_name", as: :named_route, constraints: {variable: /\d+/} root to: "home.index" get "/books", to: "books.index", as: :books get "/books/:id", to: "books.show", as: :book, id: /\d+/ post "/books", to: "books.create" scope "admin" do get "/dashboard", to: "admin.dashboard.show", as: :admin_dashboard end slice :api, at: "/api" do get "/version", to: "version.show" end redirect "/old_path", to: "/new_path", code: 301 # Default 301 end end
- Route Helpers (in actions, views via context):
routes.path(:book, id: 1)
→"/books/1"
routes.url(:book, id: 1)
→"http://localhost:2300/books/1"
(usesconfig.base_url
)
- Inspect routes:
bundle exec hanami routes
Actions (app/actions/
)
- Handle HTTP requests. Inherit from
Bookshelf::Action
. #handle(request, response)
method.# app/actions/books/show.rb module Bookshelf module Actions module Books class Show < Bookshelf::Action include Deps["repos.book_repo"] # Dependency Injection # Parameter validation (dry-validation) params do required(:id).filled(:integer) end def handle(request, response) if request.params.valid? book = book_repo.find(request.params[:id]) if book response.body = book.to_json response.format = :json else response.status = 404 response.body = {error: "not_found"}.to_json end else response.status = 422 response.body = request.params.errors.to_h.to_json response.format = :json end end end end end end
- Request Object (
request
):request.params[:key]
,request.params.dig(:nested, :key)
request.env
,request.headers
,request.session
,request.cookies
request.get?
,request.post?
,request.xhr?
- Response Object (
response
):response.body = "..."
response.status = 200
(or use symbols like:ok
)response.headers["Content-Type"] = "application/json"
response.format = :json
(setsContent-Type
based on MIME type mapping)response.redirect_to "/new_path", status: 302
response.session[:user_id] = 1
response.cookies["my_cookie"] = "value"
- Control Flow:
- Callbacks:
before :method_name_or_proc
,after :method_name_or_proc
class MyAction < Bookshelf::Action before :authenticate! def handle(req, res) # ... private def authenticate!(req, res) res.redirect_to "/login" unless req.session[:user_id] end end
- Halt:
halt 401, "Unauthorized"
(stops execution, returns response)
- Callbacks:
- MIME Types & Formats:
- Configure default:
config.actions.format :json
(app-wide) orformat :json
(action-specific). - Enables auto-parsing for JSON request bodies if
Content-Type: application/json
.
- Configure default:
- Cookies & Sessions:
- Cookies:
response.cookies["name"] = "val"
,request.cookies["name"]
. Config viaconfig.actions.cookies
. - Sessions: Enable
config.actions.sessions = :cookie, { secret: settings.session_secret, ... }
. Useresponse.session
,request.session
.
- Cookies:
- Exception Handling:
handle_exception ROM::TupleCountMismatchError => 404
handle_exception MyCustomError => :handle_my_error
- Handler method:
def handle_my_error(request, response, exception)
- Often configured in base action (
app/action.rb
).
- HTTP Caching:
response.cache_control :public, max_age: 3600
response.expires 60, :public, max_age: 600
response.fresh last_modified: @resource.updated_at
oretag: @resource.version_id
(halts with 304 if fresh)
Views & Templates
- Views prepare data for templates. Location:
app/views/
, Templates:app/templates/
. - Associated with actions by convention (e.g.,
Actions::Books::Show
→Views::Books::Show
→templates/books/show.html.erb
).
Input & Exposures
- Pass data from Action to View:
response.render(view, book_id: id)
. - Expose data to template:
# app/views/books/show.rb module Bookshelf module Views module Books class Show < Bookshelf::View include Deps["repos.book_repo"] # Dependencies expose :book do |id:| # `id:` is input from action book_repo.find(id) end expose :page_title, layout: true do |book| # Available in layout too book.title end end end end end
- In template (
.html.erb
):<h1><%= book.title %></h1>
Templates & Partials
- Layout:
app/templates/layouts/app.html.erb
(useyield
for content). - Partials:
templates/shared/_user_card.html.erb
.- Render:
<%= render "shared/user_card", user: current_user %>
- Render:
- HTML Safety: Automatic escaping. Use
<%= raw unsafe_html %>
ormy_string.html_safe
with caution.
Parts (app/views/parts/
)
- Decorate exposed objects with view-specific logic.
- Auto-associated by exposure name (e.g.,
expose :book
usesParts::Book
).# app/views/parts/book.rb module Bookshelf module Views module Parts class Book < Bookshelf::Views::Part # Or your app's base part def formatted_price helpers.format_number(price, precision: 2) # Access helpers end def author_profile_link context.routes.path(:author_profile, id: author.id) # Access context end end end end end
- In template:
<%= book.formatted_price %>
Scopes (app/views/scopes/
)
- Custom logic around a template and its locals.
# app/views/scopes/media_player.rb < Bookshelf::Views::Scope class MediaPlayer < Bookshelf::Views::Scope def play_button_label = item.playing? ? "Pause" : "Play" # `item` is a local end
- Usage in template:
<%= scope(:media_player, item: @song).render("media_player_controls") %>
or<button><%= scope(:media_player, item: @song).play_button_label %></button>
Context (app/views/context.rb
)
- Provides shared facilities (inflector, routes, request, session, flash, assets) to templates, parts, scopes.
- Customize: Define
Bookshelf::Views::Context < Hanami::View::Context
. Add methods, include Deps.
Helpers
- General purpose view methods.
- Standard:
format_number
,link_to
,image_tag
,escape_html (h)
. - Custom: Define in
app/views/helpers.rb
(moduleBookshelf::Views::Helpers
). - Access in templates/scopes directly, in parts via
helpers.
. Access context in helpers via_context
.
Rendering from Actions
- Automatic: Action renders its corresponding view, passing
request.params
as input. - Explicit:
response.render(view, my_input: value)
.
Database (hanami-db & ROM)
Configuration (DATABASE_URL
& config/providers/db.rb
)
- Uses
DATABASE_URL
environment variable. - Advanced config in
config/providers/db.rb
for ROM/Sequel plugins & extensions.# config/providers/db.rb Hanami.app.configure_provider :db do config.gateway :default do |gw| gw.adapter :sql do |sql_config| sql_config.plugin relations: :instrumentation # ROM plugin sql_config.extension :pg_json # Sequel extension end end end
Migrations (config/db/migrate/
)
- Generate:
bundle exec hanami generate migration create_users
- Structure:
ROM::SQL.migration do change do # or up/down for complex changes create_table :users do primary_key :id column :email, String, null: false, unique: true end end end
- Run:
bundle exec hanami db migrate
- Structure dump:
config/db/structure.sql
(updated bydb migrate
).
Relations (app/relations/
)
- Map to database tables/collections. Define schema and associations.
# app/relations/books.rb module Bookshelf module Relations class Books < Bookshelf::DB::Relation schema :books, infer: true do # `infer: true` reads schema from DB associations do belongs_to :author has_many :reviews end end # Custom scope def published = where(status: "published") end end end
- Querying (Sequel DSL):
books.published.where(year: 2023).order(:title).to_a
Repositories (app/repos/
)
- Mediate between your app and relations. Encapsulate data access logic.
# app/repos/book_repo.rb module Bookshelf module Repos class BookRepo < Bookshelf::DB::Repo # Define methods that use relations def find_with_author(id) books.by_pk(id).combine(:author).one end def create(attrs) books.changeset(:create, attrs).commit end end end end
Structs (app/structs/
)
- Immutable data objects returned by repositories. Can have presentation methods.
# app/structs/book.rb module Bookshelf module DB # Note: Or your app's chosen namespace for structs class Book < Hanami::DB::Struct def full_display_title "#{title} by #{author_name}" # Assuming author_name is available or fetched end end end end
Operations (app/operations/
)
- Organize business logic as a series of steps. Uses
dry-operation
. - Generate:
bundle exec hanami generate operation users.create
# app/operations/users/create.rb module Bookshelf module Operations module Users class Create < Bookshelf::Operation include Deps["repos.user_repo", "utils.mailer"] # Dependencies def call(params) validated_attrs = step validate_input(params) user = step persist_user(validated_attrs) step send_welcome_email(user) Success(user) # Final return value if all steps succeed end private def validate_input(params) # Use a validation contract or simple checks # Return Success(valid_params) or Failure(errors) schema = MyApp::Validation::Contracts::UserCreate.new # Example contract result = schema.call(params) result.success? ? Success(result.to_h) : Failure([:validation, result.errors.to_h]) end def persist_user(attrs) # Use a transaction if multiple DB operations transaction do user = user_repo.create(attrs) # other_repo.update(...) Success(user) end # rescue SomeError => e # Example error handling # Failure([:db_error, e.message]) end def send_welcome_email(user) mailer.deliver_welcome_email(user) Success() # Or Failure if mail sending can fail significantly end end end end end
- Transaction:
transaction(gateway: :my_gateway) do ... end
(rolls back on failure). - Calling:
result = Hanami.app["operations.users.create"].call(params)
- Result Handling: Pattern match on
Success(value)
orFailure([:error_type, data])
.
Assets
- Managed by
esbuild
viahanami-assets
npm package. - Source:
app/assets/
(JS injs/
, CSS incss/
, images inimages/
, etc.) - Compiled:
public/assets/
(production includes content hash in filenames). - Entry points:
app/assets/js/app.js
(default). Additional entry points:app/assets/js/admin/app.js
. - CLI:
bundle exec hanami assets compile
(for production)bundle exec hanami assets watch
(for development)
- Usage in Views (Helpers):
javascript_tag("app")
→<script src="/assets/app-HASH.js">...
stylesheet_tag("app")
→<link href="/assets/app-HASH.css">...
image_tag("logo.png")
→<img src="/assets/logo-HASH.png">...
asset_url("app.js")
→"/assets/app-HASH.js"
- CDN:
config.assets.base_url = "..."
. - Subresource Integrity:
config.assets.subresource_integrity = [:sha256]
. - Customization:
config/assets.js
(app-level) orslices/<name>/config/assets.js
(slice-level).
CLI Commands (Common)
bundle exec hanami dev
: Start development server (puma + assets watcher).bundle exec hanami console
: Start IRB/Pry console. (--engine=pry
)bundle exec hanami generate <type> <name>
:- Types:
action
,slice
,view
,operation
,component
,migration
,relation
,repo
,struct
,part
. - Example:
bundle exec hanami generate action web.posts.index --slice=web
- Types:
bundle exec hanami routes
: Display routes. (--format=csv
)bundle exec hanami db <subcommand>
:create
,drop
,migrate
,prepare
(create + migrate + seed),seed
,version
.structure dump|load
.
bundle exec hanami assets compile|watch
: Manage assets.bundle exec hanami server
: Start Puma server (code reloading enabled by default). (--no-code-reloading
to disable).bundle exec hanami middleware
: Display Rack middleware stack. (--with-arguments
)