Overview
Hanami integration encompasses the framework's approach to connecting applications with external systems, databases, and third-party services. The framework provides structured patterns for dependency injection, service objects, and adapter-based architectures that facilitate clean integration boundaries.
The core integration mechanism revolves around Hanami::Container
, which manages dependencies and provides inversion of control. This container system allows applications to register services, repositories, and adapters while maintaining loose coupling between components.
# config/app.rb
module MyApp
class App < Hanami::App
config.actions.default_response_format = :json
end
end
# Register a service in the container
Hanami.app.register("weather_service", WeatherService.new)
Hanami's repository pattern abstracts data persistence, enabling applications to swap database implementations without changing business logic. The framework supports multiple database adapters through ROM (Ruby Object Mapper), providing consistent interfaces across PostgreSQL, MySQL, SQLite, and MongoDB.
# lib/my_app/repository/user_repository.rb
module MyApp
module Repository
class UserRepository < Hanami::Repository
associations do
has_many :posts
end
def find_active
users.where(active: true)
end
end
end
end
Service integration occurs through dedicated service objects that encapsulate external API interactions, message queue operations, and complex business processes. These services integrate with the container system, allowing automatic dependency resolution and testing isolation.
# lib/my_app/services/payment_processor.rb
module MyApp
module Services
class PaymentProcessor
include Hanami::Deps["http_client", "logger"]
def process_payment(amount, token)
response = http_client.post("/charges", {
amount: amount,
source: token
})
logger.info("Payment processed: #{response.body}")
JSON.parse(response.body)
end
end
end
end
Basic Usage
Hanami applications integrate external dependencies through provider registration and dependency injection. The provider system manages service lifecycles and configuration, while the dependency injection system resolves these services automatically in actions, repositories, and other components.
Creating a basic integration starts with defining a provider that configures and registers the external service:
# config/providers/database.rb
Hanami.app.register_provider :database do
prepare do
require "sequel"
connection_string = ENV.fetch("DATABASE_URL")
target.register("database", Sequel.connect(connection_string))
end
start do
database = target["database"]
database.extension(:pg_json) if database.adapter_scheme == :postgres
end
end
Actions integrate with registered services through automatic dependency injection using the Hanami::Deps
mixin. This approach eliminates manual service instantiation while maintaining testability:
# app/actions/users/create.rb
module MyApp
module Actions
module Users
class Create < MyApp::Action
include Hanami::Deps["user_repository", "email_service"]
def handle(request, response)
user_data = request.params[:user]
user = user_repository.create(user_data)
email_service.send_welcome_email(user.email)
response.format = :json
response.body = user.to_h.to_json
end
end
end
end
end
Repository integration follows ROM conventions while providing Hanami-specific enhancements. Repositories define data access patterns and integrate with the application's domain models:
# lib/my_app/repository/order_repository.rb
module MyApp
module Repository
class OrderRepository < Hanami::Repository
commands :create, update: :by_pk, delete: :by_pk
def find_with_items(order_id)
orders.combine(:order_items).where(id: order_id).one
end
def find_by_status(status)
orders.where(status: status).to_a
end
def create_with_items(order_data, items_data)
orders.transaction do |t|
order = t.create(:orders, order_data)
items_data.each do |item_data|
item_data[:order_id] = order.id
t.create(:order_items, item_data)
end
order
end
end
end
end
end
External service integration commonly involves HTTP clients, message queues, and third-party APIs. These integrations encapsulate service-specific logic while exposing clean interfaces to the application:
# lib/my_app/services/inventory_service.rb
module MyApp
module Services
class InventoryService
include Hanami::Deps["http_client"]
BASE_URL = "https://api.inventory-system.com"
def check_availability(product_id, quantity)
response = http_client.get("#{BASE_URL}/products/#{product_id}/availability")
data = JSON.parse(response.body)
data["available_quantity"] >= quantity
rescue StandardError => error
# Fallback to local cache or default behavior
false
end
def reserve_items(product_id, quantity)
http_client.post("#{BASE_URL}/reservations", {
product_id: product_id,
quantity: quantity,
expires_at: Time.now + 3600
}.to_json)
end
end
end
end
Advanced Usage
Complex Hanami integrations involve multi-step service orchestration, advanced repository patterns, and custom middleware integration. These patterns handle scenarios requiring transaction coordination, event publishing, and sophisticated error handling across service boundaries.
Service composition enables building complex workflows from simpler service components. The container system facilitates this composition by managing service dependencies and enabling service chaining:
# lib/my_app/services/order_fulfillment_service.rb
module MyApp
module Services
class OrderFulfillmentService
include Hanami::Deps[
"inventory_service",
"payment_processor",
"shipping_service",
"notification_service",
"order_repository"
]
def fulfill_order(order_id)
order = order_repository.find(order_id)
steps = [
method(:validate_inventory),
method(:process_payment),
method(:arrange_shipping),
method(:update_order_status),
method(:send_confirmations)
]
execute_workflow(order, steps)
end
private
def execute_workflow(order, steps)
steps.each_with_object({}) do |step, context|
result = step.call(order, context)
return handle_workflow_failure(context) unless result[:success]
context.merge!(result[:data])
end
end
def validate_inventory(order, context)
available = inventory_service.check_availability(
order.product_id,
order.quantity
)
return { success: false, error: "Insufficient inventory" } unless available
reservation = inventory_service.reserve_items(order.product_id, order.quantity)
{ success: true, data: { reservation_id: reservation["id"] } }
end
def process_payment(order, context)
result = payment_processor.process_payment(order.total, order.payment_token)
{ success: result["status"] == "succeeded", data: { payment_id: result["id"] } }
end
end
end
end
Repository integration with complex domain models requires custom query methods and relationship handling. Advanced repositories implement domain-specific query patterns while maintaining separation from business logic:
# lib/my_app/repository/analytics_repository.rb
module MyApp
module Repository
class AnalyticsRepository < Hanami::Repository
def sales_report(start_date, end_date)
orders
.join(:order_items)
.join(:products)
.where(orders[:created_at] >= start_date)
.where(orders[:created_at] <= end_date)
.select {
[
products[:category].as(:category),
sum(order_items[:quantity]).as(:total_quantity),
sum(order_items[:price] * order_items[:quantity]).as(:total_revenue)
]
}
.group(:category)
.order(:total_revenue)
end
def customer_lifetime_value
orders
.join(:users)
.select {
[
users[:id].as(:customer_id),
users[:email],
count(orders[:id]).as(:order_count),
sum(orders[:total]).as(:lifetime_value)
]
}
.group(users[:id], users[:email])
.having { sum(orders[:total]) > 1000 }
end
end
end
end
Middleware integration extends Hanami applications with cross-cutting concerns like authentication, logging, and request processing. Custom middleware components integrate with the Rack stack while accessing container services:
# lib/my_app/middleware/api_authentication.rb
module MyApp
module Middleware
class ApiAuthentication
include Hanami::Deps["auth_service", "logger"]
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
return unauthorized_response unless authenticate_request(request)
user = auth_service.find_user_by_token(extract_token(request))
env["hanami.current_user"] = user
@app.call(env)
end
private
def authenticate_request(request)
token = extract_token(request)
return false unless token
auth_service.validate_token(token)
rescue AuthenticationError => error
logger.warn("Authentication failed: #{error.message}")
false
end
def extract_token(request)
auth_header = request.get_header("HTTP_AUTHORIZATION")
return unless auth_header&.start_with?("Bearer ")
auth_header.split(" ").last
end
def unauthorized_response
[401, { "Content-Type" => "application/json" }, ['{"error": "Unauthorized"}']]
end
end
end
end
Production Patterns
Production Hanami applications require robust integration patterns for monitoring, error handling, and performance optimization. These patterns ensure reliable operation under load while maintaining observability and debugging capabilities.
Database connection pooling and transaction management become critical in production environments. Hanami applications configure connection pools through ROM and implement transaction boundaries that align with business operations:
# config/providers/database.rb
Hanami.app.register_provider :database do
prepare do
require "sequel"
require "sequel/extensions/connection_validator"
database = Sequel.connect(
ENV.fetch("DATABASE_URL"),
max_connections: ENV.fetch("DB_POOL_SIZE", 20).to_i,
pool_timeout: ENV.fetch("DB_POOL_TIMEOUT", 5).to_i,
test: true,
extensions: [:connection_validator]
)
database.pool.connection_validation_timeout = -1
target.register("database", database)
end
end
# lib/my_app/services/transaction_service.rb
module MyApp
module Services
class TransactionService
include Hanami::Deps["database"]
def with_transaction(&block)
database.transaction(isolation: :repeatable_read) do
block.call
end
rescue Sequel::Rollback => error
raise BusinessLogicError, error.message
end
end
end
end
External service integration in production requires circuit breakers, timeout handling, and fallback mechanisms. These patterns prevent cascading failures while maintaining system resilience:
# lib/my_app/services/resilient_payment_service.rb
module MyApp
module Services
class ResilientPaymentService
include Hanami::Deps["http_client", "cache", "metrics"]
CIRCUIT_BREAKER_THRESHOLD = 5
CIRCUIT_BREAKER_TIMEOUT = 30
def process_payment(order)
return cached_response(order) if circuit_open?
start_time = Time.now
response = with_timeout(10) do
http_client.post("/payments", payment_payload(order))
end
record_success
metrics.increment("payment.success")
response
rescue Timeout::Error, Net::HTTPError => error
record_failure
metrics.increment("payment.failure")
fallback_payment_response(order, error)
ensure
duration = Time.now - start_time
metrics.histogram("payment.duration", duration)
end
private
def circuit_open?
failure_count > CIRCUIT_BREAKER_THRESHOLD &&
last_failure_time > Time.now - CIRCUIT_BREAKER_TIMEOUT
end
def record_failure
@failure_count = failure_count + 1
@last_failure_time = Time.now
cache.set("payment_failures", failure_count, expires_in: 300)
end
def fallback_payment_response(order, error)
{
status: "pending",
transaction_id: generate_fallback_id,
message: "Payment queued for processing",
error: error.message
}
end
end
end
end
Monitoring and observability patterns integrate with APM tools and logging systems. Production applications implement structured logging, metrics collection, and distributed tracing:
# lib/my_app/middleware/request_monitoring.rb
module MyApp
module Middleware
class RequestMonitoring
include Hanami::Deps["logger", "metrics"]
def initialize(app)
@app = app
end
def call(env)
request_id = SecureRandom.uuid
env["hanami.request_id"] = request_id
start_time = Time.now
logger.info("Request started", {
request_id: request_id,
method: env["REQUEST_METHOD"],
path: env["REQUEST_URI"],
user_agent: env["HTTP_USER_AGENT"]
})
status, headers, body = @app.call(env)
duration = Time.now - start_time
log_request_completion(request_id, status, duration)
record_metrics(env, status, duration)
[status, headers, body]
rescue StandardError => error
log_request_error(request_id, error)
metrics.increment("requests.error")
raise
end
private
def log_request_completion(request_id, status, duration)
logger.info("Request completed", {
request_id: request_id,
status: status,
duration: duration
})
end
def record_metrics(env, status, duration)
tags = {
method: env["REQUEST_METHOD"].downcase,
status: status.to_s[0],
endpoint: extract_endpoint(env)
}
metrics.increment("requests.total", tags: tags)
metrics.histogram("requests.duration", duration, tags: tags)
end
end
end
end
Reference
Core Integration Classes
Class | Purpose | Key Methods |
---|---|---|
Hanami::Container |
Dependency injection container | #register , #resolve , #[] |
Hanami::Repository |
Data access abstraction | #create , #update , #find , #delete |
Hanami::Deps |
Dependency injection mixin | #[] , #include |
Hanami::Provider |
Service lifecycle management | #prepare , #start , #stop |
Repository Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#create(data) |
data (Hash) |
Entity |
Creates new record |
#update(id, data) |
id (Integer), data (Hash) |
Entity |
Updates existing record |
#find(id) |
id (Integer) |
Entity or nil |
Finds record by primary key |
#delete(id) |
id (Integer) |
Boolean |
Removes record |
#where(conditions) |
conditions (Hash) |
Relation |
Filters records |
#order(*columns) |
columns (Symbols) |
Relation |
Orders results |
#limit(count) |
count (Integer) |
Relation |
Limits result count |
Container Registration
Registration Type | Syntax | Use Case |
---|---|---|
Simple object | container.register("key", object) |
Static services |
Factory | container.register("key") { Factory.new } |
Dynamic instantiation |
Singleton | container.register("key", memoize: true) { Service.new } |
Shared instances |
Callable | container.register("key", call: false, object) |
Non-callable objects |
Provider Lifecycle Hooks
Hook | Purpose | When Called |
---|---|---|
prepare |
Service registration | Application boot |
start |
Service initialization | After dependencies ready |
stop |
Cleanup operations | Application shutdown |
Common Integration Patterns
Pattern | Implementation | Benefits |
---|---|---|
Service Object | include Hanami::Deps["service"] |
Dependency injection |
Repository Pattern | class Repo < Hanami::Repository |
Data access abstraction |
Provider Registration | register_provider :name do...end |
Lifecycle management |
Middleware Integration | use MiddlewareClass |
Cross-cutting concerns |
Environment Configuration
Setting | Environment Variable | Default | Purpose |
---|---|---|---|
Database URL | DATABASE_URL |
- | Database connection |
Pool size | DB_POOL_SIZE |
20 | Connection pool size |
Log level | LOG_LEVEL |
info |
Logging verbosity |
Cache URL | CACHE_URL |
- | Cache connection |
Error Classes
Exception | Inheritance | When Raised |
---|---|---|
Hanami::Model::Error |
StandardError |
General model errors |
Hanami::Repository::Error |
Hanami::Model::Error |
Repository failures |
Hanami::Container::Error |
StandardError |
Container operations |
ROM::SQL::Error |
ROM::Error |
Database operations |