CrackedRuby logo

CrackedRuby

Hanami Integration

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