A Rails controller should not know how the upstream rate API authenticates or how long its responses are cached. Put that decision in one service object and return a small, explicit result to the rest of the application.
Net::HTTP from the Ruby standard library; no SDK is required.Create a small conversion service
require "net/http"
require "json"
class ExchangeRateClient
Error = Class.new(StandardError)
def convert(from:, to:, amount:)
from = from.to_s.upcase
to = to.to_s.upcase
raise Error, "invalid currency code" unless [from, to].all? { |code| code.match?(/A[A-Z]{3}z/) }
key = ["fx-convert", from, to, amount.to_d.to_s("F")]
Rails.cache.fetch(key, expires_in: 60.seconds) do
uri = URI("https://api.exchangerate.dev/v1/convert/#{from}/#{to}/#{amount}")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("EXCHANGERATE_API_KEY")}"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
open_timeout: 2, read_timeout: 5) { |http| http.request(request) }
raise Error, "exchange-rate API returned #{response.code}" unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body)
end
rescue Net::OpenTimeout, Net::ReadTimeout => error
raise Error, "exchange-rate API timed out: #{error.class}"
end
endKeep the controller thin
class ConversionsController < ApplicationController
def show
amount = BigDecimal(params.require(:amount))
result = ExchangeRateClient.new.convert(
from: params.require(:from).upcase,
to: params.require(:to).upcase,
amount: amount
)
render json: result.slice("from", "to", "amount", "rate", "converted",
"source", "market_session", "data_updated_at")
rescue ExchangeRateClient::Error, ArgumentError => error
render json: { error: error.message }, status: :bad_gateway
end
endStore evidence when the conversion becomes a record
A transient display can use a cached result. An invoice, ledger entry, or report should store the original amount, both currency codes, full-precision rate, converted amount, source, and data_updated_at. That makes the number reproducible after the live rate changes.
Use background jobs for recurring refreshes
If many users need the same base rates, fetch once in a scheduled Active Job and serve the cached snapshot. Do not let every request to a product page trigger an upstream currency request.
