# Hanami v2.2 Cheat Sheet

**URL:** <https://discourse.hanakai.org/t/hanami-v2-2-cheat-sheet/1205>\
**Category:** general\
**Created:** [May 8, 2025, 12:34pm UTC](https://discourse.hanakai.org/t/hanami-v2-2-cheat-sheet/1205 "2025-05-08T12:34:53Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![devaniljr](https://yyz2.discourse-cdn.com/flex030/user_avatar/discourse.hanakai.org/devaniljr/32/1151_2.png) [@devaniljr](https://discourse.hanakai.org/u/devaniljr)\
**Post date:** [May 8, 2025, 12:34pm UTC](https://discourse.hanakai.org/t/hanami-v2-2-cheat-sheet/1205/1 "2025-05-08T12:34:53Z")

</div>

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`

- 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):**

```ruby
# 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:**

```ruby
# 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.

```ruby
# 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 }`.

* * *

## 🌐 Request/Response Cycle

### Routing (`config/routes.rb`)

- **Define routes:**

```ruby
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.

```ruby
# 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`

```ruby
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)

* * *

## 🎨 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:

```ruby
# 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`).

```ruby
# 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.

```ruby
# 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)`.

* * *

## 💾 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.

```ruby
# 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:

```ruby
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.

```ruby
# 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.

```ruby
# 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.

```ruby
# 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`

```ruby
# 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])`.

* * *

## 🖼 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).

* * *

## ⌨ 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`)

* * *
