LearnTutorial

Exchange Rate API for Ruby on Rails

Wrap currency conversion in one Rails service, cache successful observations, and keep source and freshness metadata beside every result.

20 Sep 2026 · 7 min read

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.

  • Use Net::HTTP from the Ruby standard library; no SDK is required.
  • Store the API key in Rails credentials or an environment variable.
  • Cache by currency pair and amount policy, not by controller action.
  • Raise explicit errors for timeouts, rate limits, and unavailable data.

Create a small conversion service

ruby · app/services/exchange_rate_client.rb
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}"  endend

Keep the controller thin

ruby · conversions_controller.rb
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  endend

Store 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.

Do not silently turn failure into zero

A timeout or unavailable rate is an error state. Keep a bounded last-good value with its original timestamp, or stop the operation and ask the caller to retry.

Questions

Is there a Ruby SDK required for the exchange-rate API?

No. Rails can call the JSON endpoint with Ruby Net::HTTP. A small service object is enough to centralize authentication, timeouts, caching, and error handling.

Where should a Rails app store the API key?

Use Rails encrypted credentials or a server environment variable. Never place it in JavaScript or commit it to the repository.

Keep reading

Start with a free API key

5,000 requests a month on the free plan. No credit card.

Get a free key