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: falsecomment 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 targetgives 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
dotenvfor.envfiles 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.cookiesrequest.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-Typebased on MIME type mapping)response.redirect_to "/new_path", status: 302response.session[:user_id] = 1response.cookies["my_cookie"] = "value"
- Control Flow:
- Callbacks:
before :method_name_or_proc,after :method_name_or_procclass 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 => 404handle_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: 3600response.expires 60, :public, max_age: 600response.fresh last_modified: @resource.updated_atoretag: @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(useyieldfor 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_safewith caution.
Parts (app/views/parts/)
- Decorate exposed objects with view-specific logic.
- Auto-associated by exposure name (e.g.,
expose :bookusesParts::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.paramsas input. - Explicit:
response.render(view, my_input: value).
Database (hanami-db & ROM)
Configuration (DATABASE_URL & config/providers/db.rb)
- Uses
DATABASE_URLenvironment variable. - Advanced config in
config/providers/db.rbfor 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
esbuildviahanami-assetsnpm 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-reloadingto disable).bundle exec hanami middleware: Display Rack middleware stack. (--with-arguments)