Skip to main content
hanami 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.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/ar4mirez/samuel --skill hanami命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... 相关职业
SOC
name hanami description 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.
license MIT metadata {"author":"samuel","version":"1.0","category":"framework","language":"ruby","extensions":".rb"}
Hanami Guide
Applies to: Hanami 2.x, Ruby 3.1+, Web Applications, APIs, Domain-Driven Design, Clean Architecture
Core Principles
Clean Architecture : Strict separation between delivery (actions/views) and domain (operations/repos)
Slices as Bounded Contexts : Each slice is an isolated module with its own dependencies
Dependency Injection : Auto-injection via include Deps[...] -- no globals, no singletons
dry-rb Ecosystem : Leverage dry-types, dry-monads, dry-validation for type safety and result handling
ROM Persistence : Relations for queries, repositories for data access, entities for domain objects
Convention Over Configuration : Predictable file layout, auto-registration of components
Guardrails
Architecture
Use slices for bounded contexts (e.g., slices/api/, slices/admin/)
Keep actions thin -- delegate to operations for business logic
Operations return Dry::Monads::Result (Success/Failure), never raise for domain errors
Repositories wrap ROM relations -- never call relations directly from actions
Entities are value objects (ROM::Struct) -- keep behavior minimal
Use providers for external service registration (config/providers/)
Actions
One action per route handler (no fat controllers)
Validate params with params do ... end block inside the action
Always check request.params.valid? before processing
Use halt for early returns (401, 403, 404)
Handle exceptions with handle_exception class method
Web actions render views; API actions set response.body with JSON
Views & Templates
Views expose data to templates via expose declarations
Keep logic in view classes, not in ERB templates
Use layouts for shared page structure (config.layout = "app")
Templates use ERB by default; keep them presentation-only
Persistence (ROM)
Relations define schema, associations, and reusable query scopes
Use infer: true in relation schema to auto-detect columns
Repositories define commands (:create, update: :by_pk, delete: :by_pk)
Wrap multi-step writes in transaction blocks
Use combine for eager loading associations (avoids N+1)
Paginate with .limit().offset() -- never load unbounded datasets
Security
Validate all inputs at the action params layer
Use parameterized queries via ROM (never string interpolation)
Hash passwords with bcrypt; verify with constant-time comparison
Store secrets in settings (config/settings.rb), loaded from environment
API auth: verify JWT tokens in a base action before hook or authenticate! method
Set CSP headers via config.actions.content_security_policy
Testing
Use RSpec with rack-test for request specs
Use database_cleaner-sequel with transaction strategy
Test operations in isolation (unit tests) -- they are pure business logic
Test actions as request specs (integration) -- verify HTTP status and body
Use factory_bot for test data setup
Coverage target: >80% for operations and repositories
Project Structure myapp/
├── 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 Responsibilities 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
Quick Reference Commands
gem install hanami
hanami new myapp && cd myapp
bundle exec hanami server
bundle exec hanami console
bundle exec hanami generate slice api
bundle exec hanami generate action web.users.index
bundle exec hanami generate relation users
bundle exec hanami db create
bundle exec hanami db migrate
bundle exec hanami db seed
bundle exec rspec
bundle exec rspec --format documentation
Configuration
Application (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
Settings (config/settings.rb) 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
Routes (config/routes.rb) 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
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
Actions
Web Action
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
API Base Action (Slice)
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
API Endpoint
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
Persistence (ROM)
Relation
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
Repository
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
Migration
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
Operations (Business Logic)
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
Views & Providers
module MyApp
class View < Hanami::View
config.paths = [File .join(__dir__, "templates" )]
config.layout = "app"
expose :current_user
expose :flash
end
end
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
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
Dependencies Gem Purpose hanami (~> 2.1), -router, -controller, -viewFramework core puma (~> 6.0)Application server rom (> 5.3), rom-sql (> 3.6), pgPersistence (ROM + PostgreSQL) dry-types, dry-monads, dry-validationType system, results, validation bcrypt (> 3.1), jwt (> 2.7)Auth (password hashing, tokens) rspec, rack-test, database_cleaner-sequel, factory_botTesting
Advanced Topics For detailed patterns, validation contracts, interactors, testing strategies, assets, and deployment, see:
references/patterns.md -- ROM advanced queries, dry-validation contracts, interactor pipelines, testing patterns, asset management, deployment
External References