internal/skills/content/hanami/SKILL.md
Hanami 2+ framework guardrails, patterns, and best practices for AI-assisted development. Use when working with Hanami projects, or when the user mentions Hanami. Provides clean architecture, dry-rb integration, ROM, and modular design guidelines.
npx skillsauth add ar4mirez/samuel hanamiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Applies to: Hanami 2.x, Ruby 3.1+, Web Applications, APIs, Domain-Driven Design, Clean Architecture
include Deps[...] -- no globals, no singletonsslices/api/, slices/admin/)Dry::Monads::Result (Success/Failure), never raise for domain errorsconfig/providers/)params do ... end block inside the actionrequest.params.valid? before processinghalt for early returns (401, 403, 404)handle_exception class methodresponse.body with JSONexpose declarationsconfig.layout = "app")infer: true in relation schema to auto-detect columns:create, update: :by_pk, delete: :by_pk)transaction blockscombine for eager loading associations (avoids N+1).limit().offset() -- never load unbounded datasetsconfig/settings.rb), loaded from environmentbefore hook or authenticate! methodconfig.actions.content_security_policyrack-test for request specsdatabase_cleaner-sequel with transaction strategyfactory_bot for test data setupmyapp/
├── app/ # Main application slice
│ ├── action.rb # Base action class
│ ├── view.rb # Base view class
│ ├── actions/ # Route handlers (one class per endpoint)
│ │ └── users/
│ │ ├── index.rb
│ │ ├── show.rb
│ │ └── create.rb
│ ├── views/ # View classes (expose data to templates)
│ │ └── users/
│ │ ├── index.rb
│ │ └── show.rb
│ └── templates/ # ERB templates
│ ├── layouts/
│ │ └── app.html.erb
│ └── users/
│ ├── index.html.erb
│ └── show.html.erb
├── slices/ # Additional slices (bounded contexts)
│ └── api/
│ ├── action.rb # API base action (format :json)
│ └── actions/
│ └── v1/
│ └── users/
├── config/
│ ├── app.rb # Application configuration
│ ├── routes.rb # Route definitions
│ ├── settings.rb # Settings schema (env vars)
│ └── providers/ # Service providers
├── db/
│ ├── migrate/ # ROM migrations
│ └── seeds.rb
├── lib/
│ └── myapp/
│ ├── entities/ # ROM structs (value objects)
│ ├── repositories/ # Data access (ROM repositories)
│ ├── operations/ # Business logic (dry-monads)
│ ├── services/ # Infrastructure services
│ └── types.rb # Custom dry-types
├── spec/
│ ├── spec_helper.rb
│ ├── support/
│ ├── actions/
│ ├── operations/
│ └── repositories/
├── Gemfile
└── config.ru
| Layer | Knows About | Never References | |-------|-------------|-----------------| | Actions | Operations, views, params | Repositories, ROM directly | | Views | Exposed data, template helpers | Actions, operations | | Operations | Repositories, services, monads | Actions, views, request/response | | Repositories | ROM relations, entities | Operations, actions | | Services | External APIs, libraries | Actions, views |
# Create new app
gem install hanami
hanami new myapp && cd myapp
# Development
bundle exec hanami server # Start dev server
bundle exec hanami console # Interactive console
# Generate components
bundle exec hanami generate slice api
bundle exec hanami generate action web.users.index
bundle exec hanami generate relation users
# Database
bundle exec hanami db create
bundle exec hanami db migrate
bundle exec hanami db seed
# Testing
bundle exec rspec
bundle exec rspec --format documentation
# config/app.rb
require "hanami"
module MyApp
class App < Hanami::App
config.actions.default_response_format = :html
config.actions.content_security_policy[:default_src] = "'self'"
config.sessions = :cookie, {
key: "_myapp_session",
secret: settings.session_secret,
expire_after: 60 * 60 * 24 * 7
}
end
end
module MyApp
class Settings < Hanami::Settings
setting :database_url, constructor: Types::String
setting :session_secret, constructor: Types::String
setting :redis_url, constructor: Types::String.optional
setting :log_level, default: "info",
constructor: Types::String.enum("debug", "info", "warn", "error")
end
end
module MyApp
class Routes < Hanami::Routes
root to: "home.index"
scope "users" do
get "/", to: "users.index"
get "/new", to: "users.new"
post "/", to: "users.create"
get "/:id", to: "users.show"
patch "/:id", to: "users.update"
delete "/:id", to: "users.destroy"
end
# Mount a slice at a path prefix
slice :api, at: "/api" do
scope "v1" do
get "/users", to: "v1.users.index"
post "/users", to: "v1.users.create"
get "/users/:id", to: "v1.users.show"
end
end
end
end
# app/actions/users/create.rb
module MyApp
module Actions
module Users
class Create < MyApp::Action
include Deps["operations.users.create"]
params do
required(:user).hash do
required(:email).filled(:string)
required(:name).filled(:string)
required(:password).filled(:string, min_size?: 8)
end
end
def handle(request, response)
unless request.params.valid?
response.render(view, errors: request.params.errors)
return
end
result = create.call(request.params[:user])
if result.success?
response.flash[:success] = "User created"
response.redirect_to routes.path(:users_show, id: result.value!.id)
else
response.render(view, errors: result.failure)
end
end
end
end
end
end
# slices/api/action.rb -- JSON-only base with JWT auth and error handlers
module API
class Action < Hanami::Action
format :json
handle_exception ROM::TupleCountMismatchError => :handle_not_found
handle_exception StandardError => :handle_error
private
def authenticate!
token = request.get_header("HTTP_AUTHORIZATION")&.sub("Bearer ", "")
halt 401, { error: "Missing token" }.to_json unless token
payload = JWT.decode(token, ENV["JWT_SECRET"], true, algorithm: "HS256").first
@current_user = user_repo.find(payload["user_id"])
halt 401, { error: "Invalid token" }.to_json unless @current_user
rescue JWT::DecodeError
halt 401, { error: "Invalid token" }.to_json
end
def handle_not_found(_req, res, _ex) = (res.status = 404; res.body = { error: "Not found" }.to_json)
def handle_error(_req, res, ex) = (Hanami.logger.error(ex); res.status = 500; res.body = { error: "Internal server error" }.to_json)
end
end
# slices/api/actions/v1/users/index.rb
module API
module Actions
module V1
module Users
class Index < API::Action
include Deps["repositories.user_repo"]
params do
optional(:page).filled(:integer, gt?: 0)
optional(:per_page).filled(:integer, gt?: 0, lteq?: 100)
end
def handle(request, response)
page = request.params[:page] || 1
per_page = request.params[:per_page] || 20
users = user_repo.all_paginated(page: page, per_page: per_page)
response.body = {
users: users.map { |u| { id: u.id, email: u.email, name: u.name } },
meta: { page: page, per_page: per_page, total: user_repo.count }
}.to_json
end
end
end
end
end
end
# lib/myapp/persistence/relations/users.rb
module MyApp
module Persistence
module Relations
class Users < ROM::Relation[:sql]
schema(:users, infer: true) do
associations do
has_many :posts
end
end
def by_id(id) = where(id: id)
def by_email(e) = where(email: e.downcase)
def active = where(active: true)
def with_posts = combine(:posts)
end
end
end
end
# lib/myapp/repositories/user_repo.rb
module MyApp
module Repositories
class UserRepo < ROM::Repository[:users]
include Deps[container: "persistence.rom"]
commands :create, update: :by_pk, delete: :by_pk
def find(id) = users.by_id(id).one
def find_by_email(email) = users.by_email(email).one
def all_active = users.active.to_a
def count = users.count
def all_paginated(page:, per_page:)
users.active
.order { created_at.desc }
.limit(per_page)
.offset((page - 1) * per_page)
.to_a
end
end
end
end
# db/migrate/20240115000001_create_users.rb
ROM::SQL.migration do
change do
create_table :users do
primary_key :id
column :email, String, null: false, unique: true
column :name, String, null: false
column :password_digest, String, null: false
column :role, String, default: "user"
column :active, TrueClass, default: true
column :created_at, DateTime, null: false
column :updated_at, DateTime, null: false
end
add_index :users, :email, unique: true
end
end
# lib/myapp/operations/users/create.rb
require "dry/monads"
module MyApp
module Operations
module Users
class Create
include Dry::Monads[:result]
include Deps["repositories.user_repo", "services.password_hasher"]
def call(params)
return Failure(email: ["already taken"]) if user_repo.find_by_email(params[:email])
user = user_repo.create(
email: params[:email].downcase.strip,
name: params[:name].strip,
password_digest: password_hasher.hash(params[:password]),
created_at: Time.now,
updated_at: Time.now
)
Success(user)
rescue => e
Hanami.logger.error(e)
Failure(base: ["An unexpected error occurred"])
end
end
end
end
end
# app/view.rb -- Base view: set layout, expose shared data
module MyApp
class View < Hanami::View
config.paths = [File.join(__dir__, "templates")]
config.layout = "app"
expose :current_user
expose :flash
end
end
# app/views/users/show.rb -- Expose user, add helper methods for templates
module MyApp
module Views
module Users
class Show < MyApp::View
expose :user
private
def user_posts(user) = user.posts.select(&:published?)
end
end
end
end
# config/providers/services.rb -- Register services in the DI container
Hanami.app.register_provider :services do
start do
register "services.password_hasher", MyApp::Services::PasswordHasher.new
register "services.jwt_encoder", MyApp::Services::JWTEncoder.new
end
end
| Gem | Purpose |
|-----|---------|
| hanami (~> 2.1), -router, -controller, -view | Framework core |
| puma (~> 6.0) | Application server |
| rom (~> 5.3), rom-sql (~> 3.6), pg | Persistence (ROM + PostgreSQL) |
| dry-types, dry-monads, dry-validation | Type system, results, validation |
| bcrypt (~> 3.1), jwt (~> 2.7) | Auth (password hashing, tokens) |
| rspec, rack-test, database_cleaner-sequel, factory_bot | Testing |
For detailed patterns, validation contracts, interactors, testing strategies, assets, and deployment, see:
development
Zig language guardrails, patterns, and best practices for AI-assisted development. Use when working with Zig files (.zig), build.zig, or when the user mentions Zig. Provides comptime patterns, allocator conventions, C interop guidelines, and testing standards specific to this project's coding standards.
tools
WordPress framework guardrails, patterns, and best practices for AI-assisted development. Use when working with WordPress projects, or when the user mentions WordPress. Provides theme development, plugin architecture, REST API, blocks, and security guidelines.
tools
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. Use when testing web apps, automating browser interactions, or debugging frontend issues.
tools
Suite of tools for creating elaborate, multi-component web applications using modern frontend technologies (React, Tailwind CSS, shadcn/ui). Use for complex projects requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX pages.