Skip to content

Client Strategy ​

The client strategy provides a declarative way to configure HTTP clients for API integrations. It creates a memoized Faraday connection with sensible defaults and optional error handling.

Peer Dependency

This strategy requires the faraday gem. It is always registered — regardless of whether Faraday is loaded, and regardless of gem load order — and requires faraday when you declare it, so use :client raises a LoadError naming the missing gem if it isn't in your bundle. Add gem "faraday", "~> 2.0" to your Gemfile to use it.

Basic Usage ​

ruby
class FetchUserData
  include Axn

  use :client, url: "https://api.example.com"

  expects :user_id
  exposes :user_data

  def call
    response = client.get("/users/#{user_id}")
    expose user_data: response.body
  end
end

Configuration Options ​

OptionDefaultDescription
name:clientThe method name for accessing the client
url(none)Base URL for the API. Not enforced by the strategy — it's passed through to Faraday.new, so it's effectively required only if you make relative requests.
headers{}Default headers to include in all requests (passed through to Faraday.new)
user_agentAuto-generatedCustom User-Agent header
debugfalseEnable Faraday response logging
prepend_confignilProc to prepend middleware configuration
error_handlernilError handling configuration (see below)

Any additional options are passed directly to Faraday.new.

Default Middleware ​

The client strategy sets these headers and middleware on the Faraday connection.

Headers:

  • Content-Type: application/json
  • User-Agent (configurable via user_agent:)

Middleware (registered in this order; any prepend_config proc runs before them):

  1. An execution-context middleware that records the last request/response so it can be attached to exception reports
  2. response :raise_error - Raises on 4xx/5xx responses
  3. request :url_encoded - Encodes request parameters
  4. request :json - JSON request encoding
  5. response :json - JSON response parsing (only for json content types)
  6. response :logger - Response logging, only when debug: true

Custom Client Name ​

ruby
class ExternalApiAction
  include Axn

  use :client, name: :api_client, url: "https://api.example.com"
  use :client, name: :auth_client, url: "https://auth.example.com"

  def call
    token = auth_client.post("/token").body["access_token"]
    data = api_client.get("/data", nil, { "Authorization" => "Bearer #{token}" })
    # ...
  end
end

Dynamic Configuration ​

Options can be callables (procs/lambdas) for dynamic values:

ruby
class SecureApiAction
  include Axn

  use :client,
    url: "https://api.example.com",
    headers: -> { { "Authorization" => "Bearer #{current_token}" } } 

  private

  def current_token
    # Fetch or refresh token as needed
    TokenStore.get_valid_token
  end
end

Custom Headers ​

ruby
class ApiAction
  include Axn

  use :client,
    url: "https://api.example.com",
    headers: {
      "X-API-Key" => ENV["API_KEY"],
      "Accept" => "application/json"
    }
end

Error Handling ​

The error_handler option configures custom error handling middleware:

ruby
class ApiAction
  include Axn

  use :client,
    url: "https://api.example.com",
    error_handler: {
      if: -> { status != 200 },           # Condition to trigger error handling
      error_key: "error.message",          # JSON path to error message
      detail_key: "error.details",         # JSON path to error details (optional)
      backtrace_key: "error.backtrace",    # JSON path to backtrace (optional)
      exception_class: CustomApiError,     # Exception class to raise (default: Faraday::BadRequestError)
      formatter: ->(error, details, env) { # Custom message formatter (optional)
        "API Error: #{error} - #{details}"
      },
      extract_detail: ->(key, value) {     # Extract detail from hash/array (optional)
        "#{key}: #{value}"
      }
    }
end

Error Handler Options ​

OptionDescription
ifCondition proc to trigger error handling (receives status, body, response_env)
error_keyDot-notation path to error message in response JSON
detail_keyDot-notation path to error details
backtrace_keyDot-notation path to backtrace
exception_classException class to raise (default: Faraday::BadRequestError)
formatterCustom proc to format the error message
extract_detailProc to extract details from nested structures

Prepending Middleware ​

Use prepend_config when you need to add middleware before the default stack:

ruby
class ApiAction
  include Axn

  use :client,
    url: "https://api.example.com",
    prepend_config: ->(conn) {
      conn.request :retry, max: 3, interval: 0.5
      conn.request :authorization, "Bearer", -> { fetch_token }
    }
end

Complete Example ​

ruby
class SyncExternalData
  include Axn

  use :client,
    name: :external_api,
    url: ENV["EXTERNAL_API_URL"],
    headers: -> { { "Authorization" => "Bearer #{api_token}" } },
    user_agent: "MyApp/1.0",
    error_handler: {
      error_key: "error.message",
      detail_key: "error.details",
      extract_detail: ->(node) { node["field"] ? "#{node['field']}: #{node['message']}" : node["message"] }
    }

  expects :company, model: Company
  exposes :synced_records

  error "Failed to sync external data"
  error(if: Faraday::BadRequestError) { |e| "External API error: #{e.message}" }

  def call
    response = external_api.get("/companies/#{company.external_id}/data")
    records = response.body["records"].map do |record|
      company.external_records.find_or_create_by!(external_id: record["id"]) do |r|
        r.data = record
      end
    end
    expose synced_records: records
  end

  private

  def api_token
    Rails.cache.fetch("external_api_token", expires_in: 1.hour) do
      # Token refresh logic
    end
  end
end

Memoization ​

The client is automatically memoized using memo, so repeated calls to the client method return the same Faraday connection instance. This ensures efficient connection reuse within a single action execution.

See Also ​