Expert guidance on building, configuring, and testing Hanami Actions
This skill provides expert guidance on building, configuring, and testing Hanami Actions (v2.x). It covers parameter handling, response management, HTTP features, exception handling, control flow patterns, and testing strategies.
Create a basic action
Hanami::Action or app base class#handle(request, response) methodAccess request data
request.params for query/path/body parametersrequest.headers[:header_name] or request.get_header("HEADER_NAME")request.envrequest.params.dig(:key, :nested_key)Build responses
response.body = dataresponse.status = 201 or response.status = :createdresponse.headers["X-Custom"] = "value"response.format = :jsonDefine parameter schemas
params do
required(:email).filled(:string)
optional(:page).value(:integer, gteq?: 1)
required(:address).hash do
required(:street).filled(:string)
end
end
Validate and handle errors
request.params.valid?request.params.errorshalt 422 unless request.params.valid?halt 422, {errors: request.params.errors}.to_jsonUse concrete parameter classes
Hanami::Action::Paramsparams Params::CreateConfigure accepted formats
# App-wide configuration
config.actions.formats.accept :json, :html
# Per-action configuration
config.formats.accept :json
Set response format dynamically
response.format = :json or response.format = "application/json"Register custom formats
config.actions.formats.register :custom, "application/custom"
config.actions.formats.add :json, ["application/json+scim", "application/json"]
Manage cookies
response.cookies["name"] = "value"response.cookies["name"] = nilconfig.actions.cookies = nilConfigure sessions
config.actions.sessions = :cookie, {
key: "app.session",
secret: settings.session_secret,
expire_after: 3600
}
response.session[:key] = valuerequest.session[:key]Set Content Security Policy
# Modify existing directive
config.actions.content_security_policy[:script_src] += " https://cdn.example.com"
# Replace directive entirely
config.actions.content_security_policy[:style_src] = "https://cdn.example.com"
# Disable CSP
config.actions.content_security_policy = false
# Use nonces for inline scripts
config.actions.content_security_policy[:script_src] = "'self' 'nonce'"
Implement HTTP caching
response.cache_control :public, max_age: 600response.expires 60, :public, max_age: 600response.fresh last_modified: time or response.fresh etag: "value"Define callbacks
before :authenticate_user!
after :log_request
before { |request, response| halt 422 unless valid?(request) }
Halt execution
halt 401 returns status with default messagehalt 401, "Custom message" sets custom bodyRedirect requests
response.redirect_to("/sign-in")
response.redirect_to(routes.path(:sign_in), status: 301)
Handle exceptions in actions
handle_exception StandardError => 500
handle_exception RecordNotFound => :handle_not_found
def handle_not_found(request, response, exception)
response.status = 404
response.body = "Resource not found"
end
Use string class names for exceptions from dependencies:
handle_exception "Stripe::CardError" => 400Create base action classes
# apps/action.rb
module Bookshelf
class Action < Hanami::Action
include Deps["authenticator"]
format :json
before :authenticate_user!
private
def authenticate_user!(request, response)
halt 401 unless authenticator.valid?(request)
end
end
end
Use modules for shared behavior
module AuthenticatedAction
def self.included(action_class)
action_class.before :authenticate_user!
end
private
def authenticate_user!(request, response)
# implementation
end
end
class Update < Action
include AuthenticatedAction
end
Test actions directly
RSpec.describe Books::Index do
subject(:action) { Books::Index.new }
it "returns successful response" do
response = action.call({})
expect(response).to be_successful
end
it "accepts params and headers" do
response = action.call(id: "23", "HTTP_ACCEPT" => "application/json")
expect(response.headers["Content-Type"]).to eq("application/json; charset=utf-8")
end
end
Inject test dependencies
subject(:action) { Books::Create.new(user_repo: user_repo) }
let(:user_repo) do
instance_double(UserRepo).as_null_object
end
it "uses injected dependency" do
expect(user_repo).to receive(:create).with(book_params)
action.call(book: book_params)
end
Write request specs
RSpec.describe "Books", type: :request do
it "returns list of books" do
get "/books"
expect(last_response).to be_successful
expect(JSON.parse(last_response.body)).to be_an(Array)
end
end
Access Rack environment
request.env["REQUEST_METHOD"] for raw HTTP datarequest.path_info, request.user_agent, etc. via Rack::Request methodsConfigure middleware
# App-level middleware
config.middleware.use Rack::Auth::Basic
# Insert at specific position
config.middleware.use Rack::ShowStatus, before: Rack::Auth::Basic
# Route-specific middleware
scope "admin" do
use Rack::Auth::Basic
get "/books", to: "books.index"
end
When assisting with Hanami Actions tasks, follow this workflow:
Identify the action type and requirements
Recommend appropriate patterns
Provide implementation guidance
Address HTTP feature needs
Guide testing implementation
Review and refine
When detailed information is needed about specific topics, consult the Hanami Actions documentation:
hanami/action over direct Rack integration when possible:ok, :created) for readability