Hanami v2.2 Cheat Sheet

I’ve asked Gemini to summarize Hanami 2.2 Guides, so I will share with you:

Hanami v2.2 Cheat Sheet

:rocket: 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
  • Start development server: cd my_app_name && bundle exec hanami dev (runs server & assets watcher via Procfile.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 ... or config.slices = [...] in config/app.rb.

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/ and slices/<name>/ are auto-registered as components.
  • Key Naming: Namespace::MyClass → "namespace.my_class".
  • Accessing: Hanami.app["component.key"] or MySlice::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/ and lib/<app_name>/ are autoloaded.
  • Other lib/ dirs, gems, and some config/ files need explicit require.
  • Inflections: config.inflections { |i| i.acronym "API" }.

Environments

  • HANAMI_ENV (or RACK_ENV). Defaults to :development.
  • Hanami.env (returns symbol), Hanami.env?(:production).
  • Env-specific config: environment(:production) { config.logger.level = :info }.

:globe_with_meridians: 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" (uses config.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 (sets Content-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)
  • MIME Types & Formats:
    • Configure default: config.actions.format :json (app-wide) or format :json (action-specific).
    • Enables auto-parsing for JSON request bodies if Content-Type: application/json.
  • Cookies & Sessions:
    • Cookies: response.cookies["name"] = "val", request.cookies["name"]. Config via config.actions.cookies.
    • Sessions: Enable config.actions.sessions = :cookie, { secret: settings.session_secret, ... }. Use response.session, request.session.
  • 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 or etag: @resource.version_id (halts with 304 if fresh)

:artist_palette: 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 (use yield for content).
  • Partials: templates/shared/_user_card.html.erb.
    • Render: <%= render "shared/user_card", user: current_user %>
  • HTML Safety: Automatic escaping. Use <%= raw unsafe_html %> or my_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 uses Parts::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 (module Bookshelf::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).

:floppy_disk: 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 by db 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
    

:hammer_and_wrench: 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) or Failure([:error_type, data]).

:framed_picture: Assets

  • Managed by esbuild via hanami-assets npm package.
  • Source: app/assets/ (JS in js/, CSS in css/, images in images/, 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) or slices/<name>/config/assets.js (slice-level).

:keyboard: 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
  • 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)