Code Examples
Ruby on Rails and the CurrencyCore API
Call /v1/convert and /v1/rates from Rails 7 with Faraday, credentials, explicit timeouts, a named error class, and a date-keyed cache.
There is no official Ruby SDK, so Faraday or Net::HTTP is the supported path. Rails 7 and Faraday 2 keep this small: one initializer owns the connection, one error class carries the API code and status, and one cache key follows the daily snapshot. timeout is Faraday’s read timeout; open_timeout covers the TCP and TLS handshake. Setting both stops a stalled response from occupying a Puma thread indefinitely.
Add Faraday to the Gemfile. Version 2 defaults to the Net::HTTP adapter and includes JSON parsing:
gem "faraday", "~> 2.9"
Store the key in Rails credentials, with an environment variable as the fallback. The initializer runs once per process:
# config/initializers/currency_core.rb
module CurrencyCore
API_KEY = Rails.application.credentials.dig(:currencycore, :api_key) ||
ENV.fetch("CURRENCYCORE_API_KEY")
CONNECTION = Faraday.new(url: "https://api.currency-core.com/v1/") do |f|
f.options.open_timeout = 3
f.options.timeout = 10
f.headers["Authorization"] = "Bearer #{API_KEY}"
f.headers["Accept"] = "application/json"
f.response :json
end
end
The error class keeps the upstream status and code, so the controller can distinguish input errors from plan gates and rate limits:
module CurrencyCore
class Error < StandardError
attr_reader :status, :code
def initialize(status:, code:, message:)
@status = status
@code = code
super(message)
end
end
module Client
module_function
def convert(from:, to:, amount:, date: nil)
response = CONNECTION.get("convert") do |req|
req.params["from"] = from
req.params["to"] = Array(to).join(",")
req.params["amount"] = amount.to_s
req.params["date"] = date if date
end
parse(response)
rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
raise Error.new(status: 504, code: "upstream_timeout", message: e.message)
end
def rates(base:, date: Date.current.iso8601)
key = "currencycore:rates:#{base}:#{date}"
ttl = date < Date.current.iso8601 ? 30.days : 10.minutes
Rails.cache.fetch(key, expires_in: ttl) do
response = CONNECTION.get("rates/#{base}") do |req|
req.params["date"] = date
end
parse(response)
end
end
def parse(response)
body = response.body
unless response.success?
error = body.is_a?(Hash) ? body["error"] : nil
raise Error.new(
status: response.status,
code: error ? error["code"] : "http_error",
message: error ? error["message"] : "HTTP #{response.status}"
)
end
body
end
end
end
The controller returns the upstream status rather than a generic 500:
class CurrencyController < ApplicationController
def convert
render json: CurrencyCore::Client.convert(
from: params.fetch(:from, "USD"),
to: params.fetch(:to, "INR"),
amount: params.fetch(:amount, "100"),
date: params[:date]
)
rescue CurrencyCore::Error => e
render json: { error: { code: e.code, message: e.message } }, status: e.status
end
end
A conversion returns one entry per target. A rates response returns a map with the base at 1:
{
"from": "USD",
"amount": 100,
"date": "2026-11-10",
"results": [{ "to": "INR", "result": 8345.0, "rate": 83.45 }]
}
{ "base": "EUR", "date": "2026-11-10", "rates": { "EUR": 1, "USD": 1.0861 } }
Free allows 1,000 requests per month at 60 per minute. Test keys use the same data and quota, and live keys require a paid plan. Rates are polled hourly, and the fixings are daily.
Production notes
If you use Net::HTTP instead of Faraday, pass timeouts to Net::HTTP.start:
Net::HTTP.start(uri.host, uri.port, use_ssl: true,
open_timeout: 3, read_timeout: 10, write_timeout: 10) do |http|
# ...
end
open_timeout bounds connection setup. read_timeout applies to each socket read, not the whole exchange, so a slow trickle can exceed your wall-clock budget. Faraday’s timeout maps to the read timeout; add a request deadline if total latency matters.
Rails.cache with :memory_store is per process; use Redis or Memcached across workers. Historical dates are stable for normal caching and can live 30 days; keep the observation date. The latest snapshot moves about once per business day, so 10 minutes is enough.
The API sends no ETag, Last-Modified, or quota headers. Track usage yourself, retry only 429 and 5xx, and honor Retry-After. A history 403 usually means the date is outside the Free 30-day window.