Hanami Actions & Validation
Hanami validates incoming params with a schema/contract before your action body ever runs — invalid input never reaches your domain logic, and there’s no manual params.require(:x).permit(:y) juggling.
Inline params schema
module MyApp
module Actions
module Posts
class Create < MyApp::Action
params do
required(:title).filled(:string)
optional(:body).maybe(:string)
required(:status).value(included_in?: %w[draft published])
end
def handle(request, response)
halt 422 unless request.params.valid?
post = post_repo.create(request.params.to_h)
response.redirect_to "/posts/#{post.id}"
end
end
end
end
end
The schema DSL is dry-schema under the hood — required/optional, filled/maybe, type checks (:string, :integer), and predicate checks (included_in?, gt?, etc.) all compose.
Standalone contracts
For validation logic reused across multiple actions (or that needs custom rules beyond shape checking), pull it into its own class:
module MyApp
module Actions
module Posts
class CreateContract < MyApp::Action::Contract
params do
required(:title).filled(:string, max_size?: 200)
end
rule(:title) do
key.failure("must be unique") if MyApp::Container["repositories.post_repo"].title_taken?(value)
end
end
end
end
end
rule blocks handle validation that needs to reach outside the shape of the input itself — DB uniqueness checks, cross-field consistency, and so on.
Accessing validated params
request.params[:title] # raw or coerced value
request.params.to_h # whole validated, symbol-keyed hash
request.params.valid? # true/false
request.params.errors # error messages, keyed by field
Only fields declared in the schema are permitted through — anything else submitted is silently dropped, the same protection strong_parameters gives you in Rails, but declared once per action instead of inline per controller action.
Halting the request
halt 404 # not found, empty body
halt 422, { errors: request.params.errors.to_h }.to_json
halt 401 unless authenticated?
halt immediately stops the action and returns the given status — no need for an explicit return after it.
Callbacks
class Show < MyApp::Action
before :require_authentication!
private
def require_authentication!(request, response)
halt 401 unless request.session[:user_id]
end
end
before/after callbacks run in declaration order, same idea as Rails’ before_action, but scoped to a single action class rather than shared implicitly across a whole controller.
Rendering an error response
def handle(request, response)
result = post_repo.create(request.params.to_h)
response.render(view, post: result)
rescue MyApp::ValidationError => e
response.status = 422
response.render(error_view, errors: e.errors)
end
Actions are plain Ruby objects, so ordinary rescue works exactly as you’d expect — no separate rescue_from registry to keep in sync with the rest of the codebase.