Skip to main content

Rust Examples

Complete Rust examples for integrating OpenFXRates using reqwest and async patterns.

Installation

Add the following dependencies to your Cargo.toml:

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenv = "0.15"
anyhow = "1.0"

Example 1: Get Latest Rates

use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::error::Error;

#[derive(Debug, Deserialize, Serialize)]
struct LatestRatesResponse {
base: String,
date: String,
rates: HashMap<String, f64>,
}

const API_KEY: &str = "your-api-key-here";
const BASE_URL: &str = "https://api.openfxrates.com";

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
get_latest_rates("USD", "EUR,GBP,JPY").await?;
Ok(())
}

async fn get_latest_rates(base: &str, targets: &str) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();

let url = format!("{}/latest_rates", BASE_URL);

let response = client
.get(&url)
.header("X-API-Key", API_KEY)
.header("Content-Type", "application/json")
.query(&[("base", base), ("targets", targets)])
.send()
.await?;

if response.status().is_success() {
let data: LatestRatesResponse = response.json().await?;

println!("Base: {}", data.base);
println!("Date: {}", data.date);
println!("Rates:");

for (currency, rate) in data.rates {
println!(" {}: {:.4}", currency, rate);
}
} else {
eprintln!("Error: HTTP {}", response.status());
eprintln!("{}", response.text().await?);
}

Ok(())
}

Example 2: Convert Currency

use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::error::Error;

#[derive(Debug, Deserialize, Serialize)]
struct ConversionResponse {
from: String,
amount: f64,
conversions: HashMap<String, f64>,
}

const API_KEY: &str = "your-api-key-here";
const BASE_URL: &str = "https://api.openfxrates.com";

async fn convert_currency(
from: &str,
to: &str,
amount: f64
) -> Result<ConversionResponse, Box<dyn Error>> {
let client = reqwest::Client::new();

let response = client
.get(format!("{}/convert", BASE_URL))
.header("X-API-Key", API_KEY)
.query(&[
("from", from),
("to", to),
("amount", &amount.to_string()),
])
.send()
.await?;

if response.status().is_success() {
let data: ConversionResponse = response.json().await?;

println!("Converting: {:.2} {}", data.amount, data.from);
println!("Results:");

for (currency, converted) in &data.conversions {
println!(" {}: {:.2}", currency, converted);
}

Ok(data)
} else {
let error_text = response.text().await?;
Err(format!("API Error: {}", error_text).into())
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
convert_currency("USD", "EUR,GBP,JPY", 100.0).await?;
Ok(())
}

Example 3: Get Historical Rates

use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::error::Error;

#[derive(Debug, Deserialize, Serialize)]
struct HistoricalRatesResponse {
base: String,
date: String,
rates: HashMap<String, f64>,
}

const API_KEY: &str = "your-api-key-here";
const BASE_URL: &str = "https://api.openfxrates.com";

async fn get_historical_rates(
base: &str,
date: &str,
targets: &str,
) -> Result<HistoricalRatesResponse, Box<dyn Error>> {
let client = reqwest::Client::new();

let response = client
.get(format!("{}/historical_rates", BASE_URL))
.header("X-API-Key", API_KEY)
.query(&[
("base", base),
("date", date),
("targets", targets),
])
.send()
.await?;

if response.status().is_success() {
let data: HistoricalRatesResponse = response.json().await?;

println!("Historical rates for {}", data.date);
println!("Base: {}", data.base);
println!("Rates:");

for (currency, rate) in &data.rates {
println!(" {}: {:.4}", currency, rate);
}

Ok(data)
} else {
Err(format!("API Error: {}", response.status()).into())
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Get rates from January 15, 2024
get_historical_rates("USD", "2024-01-15", "EUR,GBP").await?;
Ok(())
}

Example 4: List All Currencies

use reqwest;
use serde::{Deserialize, Serialize};
use std::error::Error;

#[derive(Debug, Deserialize, Serialize)]
struct Currency {
code: String,
name: String,
symbol: Option<String>,
}

#[derive(Debug, Deserialize, Serialize)]
struct CurrenciesResponse {
currencies: Vec<Currency>,
}

const API_KEY: &str = "your-api-key-here";
const BASE_URL: &str = "https://api.openfxrates.com";

async fn list_currencies() -> Result<Vec<Currency>, Box<dyn Error>> {
let client = reqwest::Client::new();

let response = client
.get(format!("{}/currencies", BASE_URL))
.header("X-API-Key", API_KEY)
.send()
.await?;

if response.status().is_success() {
let data: CurrenciesResponse = response.json().await?;

println!("Available currencies: {}\n", data.currencies.len());

for currency in &data.currencies {
let symbol = currency.symbol
.as_ref()
.map(|s| format!(" ({})", s))
.unwrap_or_default();

println!("{} - {}{}", currency.code, currency.name, symbol);
}

Ok(data.currencies)
} else {
Err(format!("API Error: {}", response.status()).into())
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
list_currencies().await?;
Ok(())
}

Example 5: API Client Struct

Complete reusable client struct for OpenFXRates:

use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use anyhow::{Result, Context};

#[derive(Debug, Deserialize, Serialize)]
pub struct LatestRatesResponse {
pub base: String,
pub date: String,
pub rates: HashMap<String, f64>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ConversionResponse {
pub from: String,
pub amount: f64,
pub conversions: HashMap<String, f64>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Currency {
pub code: String,
pub name: String,
pub symbol: Option<String>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct CurrenciesResponse {
pub currencies: Vec<Currency>,
}

pub struct OpenFXRatesClient {
client: reqwest::Client,
api_key: String,
base_url: String,
}

impl OpenFXRatesClient {
pub fn new(api_key: String) -> Result<Self> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.context("Failed to create HTTP client")?;

Ok(Self {
client,
api_key,
base_url: "https://api.openfxrates.com".to_string(),
})
}

pub fn from_env() -> Result<Self> {
dotenv::dotenv().ok();
let api_key = std::env::var("OPENFXRATES_API_KEY")
.context("OPENFXRATES_API_KEY not found in environment")?;
Self::new(api_key)
}

pub async fn get_latest_rates(
&self,
base: &str,
targets: Option<&str>,
) -> Result<LatestRatesResponse> {
let mut url = format!("{}/latest_rates", self.base_url);

let mut params = vec![("base", base)];
if let Some(targets) = targets {
params.push(("targets", targets));
}

let response = self
.client
.get(&url)
.header("X-API-Key", &self.api_key)
.header("Content-Type", "application/json")
.query(&params)
.send()
.await
.context("Failed to send request")?;

if response.status().is_success() {
response
.json()
.await
.context("Failed to parse response")
} else {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
Err(anyhow::anyhow!("API Error {}: {}", status, error_text))
}
}

pub async fn get_historical_rates(
&self,
base: &str,
date: &str,
targets: Option<&str>,
) -> Result<LatestRatesResponse> {
let mut params = vec![("base", base), ("date", date)];
if let Some(targets) = targets {
params.push(("targets", targets));
}

let response = self
.client
.get(format!("{}/historical_rates", self.base_url))
.header("X-API-Key", &self.api_key)
.query(&params)
.send()
.await
.context("Failed to send request")?;

if response.status().is_success() {
response
.json()
.await
.context("Failed to parse response")
} else {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
Err(anyhow::anyhow!("API Error {}: {}", status, error_text))
}
}

pub async fn convert_currency(
&self,
from: &str,
to: &str,
amount: f64,
) -> Result<ConversionResponse> {
let response = self
.client
.get(format!("{}/convert", self.base_url))
.header("X-API-Key", &self.api_key)
.query(&[
("from", from),
("to", to),
("amount", &amount.to_string()),
])
.send()
.await
.context("Failed to send request")?;

if response.status().is_success() {
response
.json()
.await
.context("Failed to parse response")
} else {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
Err(anyhow::anyhow!("API Error {}: {}", status, error_text))
}
}

pub async fn list_currencies(&self) -> Result<Vec<Currency>> {
let response = self
.client
.get(format!("{}/currencies", self.base_url))
.header("X-API-Key", &self.api_key)
.send()
.await
.context("Failed to send request")?;

if response.status().is_success() {
let data: CurrenciesResponse = response
.json()
.await
.context("Failed to parse response")?;
Ok(data.currencies)
} else {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
Err(anyhow::anyhow!("API Error {}: {}", status, error_text))
}
}
}

Using the Client Struct

use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
let client = OpenFXRatesClient::from_env()?;

// Get latest rates
let latest = client.get_latest_rates("USD", Some("EUR,GBP,JPY")).await?;
println!("Base: {}", latest.base);
println!("Rates: {:?}", latest.rates);

// Convert currency
let conversion = client.convert_currency("USD", "EUR", 100.0).await?;
println!("Conversions: {:?}", conversion.conversions);

// Get historical rates
let historical = client
.get_historical_rates("USD", "2024-01-15", Some("EUR,GBP"))
.await?;
println!("Historical date: {}", historical.date);
println!("Rates: {:?}", historical.rates);

// List all currencies
let currencies = client.list_currencies().await?;
println!("Total currencies: {}", currencies.len());

Ok(())
}

Example 6: Concurrent Requests

Making multiple API calls concurrently:

use tokio;
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
let client = OpenFXRatesClient::from_env()?;

// Make multiple requests concurrently
let (usd_rates, eur_rates, gbp_rates) = tokio::try_join!(
client.get_latest_rates("USD", Some("EUR,GBP,JPY")),
client.get_latest_rates("EUR", Some("USD,GBP,JPY")),
client.get_latest_rates("GBP", Some("USD,EUR,JPY"))
)?;

println!("USD Rates: {:?}", usd_rates.rates);
println!("EUR Rates: {:?}", eur_rates.rates);
println!("GBP Rates: {:?}", gbp_rates.rates);

Ok(())
}

Example 7: Error Handling with Custom Types

use thiserror::Error;
use serde::Deserialize;

#[derive(Error, Debug)]
pub enum ApiError {
#[error("HTTP request failed: {0}")]
RequestFailed(#[from] reqwest::Error),

#[error("API returned error ({status}): {message}")]
ApiError { status: u16, message: String },

#[error("Failed to parse response: {0}")]
ParseError(String),

#[error("Invalid API key")]
InvalidApiKey,

#[error("Rate limit exceeded")]
RateLimitExceeded,
}

#[derive(Debug, Deserialize)]
struct ErrorResponse {
message: String,
#[serde(default)]
code: Option<String>,
}

impl OpenFXRatesClient {
async fn handle_response<T: serde::de::DeserializeOwned>(
&self,
response: reqwest::Response,
) -> Result<T, ApiError> {
let status = response.status();

match status.as_u16() {
200..=299 => {
response.json().await.map_err(|e| {
ApiError::ParseError(e.to_string())
})
}
401 => Err(ApiError::InvalidApiKey),
429 => Err(ApiError::RateLimitExceeded),
_ => {
let error_body: ErrorResponse = response
.json()
.await
.unwrap_or_else(|_| ErrorResponse {
message: "Unknown error".to_string(),
code: None,
});

Err(ApiError::ApiError {
status: status.as_u16(),
message: error_body.message,
})
}
}
}
}

// Usage
#[tokio::main]
async fn main() {
let client = OpenFXRatesClient::from_env().unwrap();

match client.get_latest_rates("USD", Some("EUR,GBP")).await {
Ok(data) => println!("Success: {:?}", data),
Err(ApiError::InvalidApiKey) => {
eprintln!("Error: Invalid API key. Please check your credentials.");
}
Err(ApiError::RateLimitExceeded) => {
eprintln!("Error: Rate limit exceeded. Please try again later.");
}
Err(e) => eprintln!("Error: {}", e),
}
}

Example 8: Retry Logic with Backoff

use tokio::time::{sleep, Duration};
use anyhow::Result;

async fn with_retry<F, Fut, T>(
max_retries: u32,
operation: F,
) -> Result<T>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let mut attempts = 0;

loop {
match operation().await {
Ok(result) => return Ok(result),
Err(e) if attempts < max_retries => {
attempts += 1;
let delay = Duration::from_secs(2_u64.pow(attempts));
eprintln!("Request failed, retrying in {:?}... (attempt {}/{})",
delay, attempts, max_retries);
sleep(delay).await;
}
Err(e) => return Err(e),
}
}
}

// Usage
#[tokio::main]
async fn main() -> Result<()> {
let client = OpenFXRatesClient::from_env()?;

let data = with_retry(3, || {
client.get_latest_rates("USD", Some("EUR,GBP,JPY"))
}).await?;

println!("Success: {:?}", data);
Ok(())
}

Best Practices

1. Environment Variables

Create a .env file:

OPENFXRATES_API_KEY=your-api-key-here

Load it in your code:

use dotenv::dotenv;
use std::env;

fn main() {
dotenv().ok();
let api_key = env::var("OPENFXRATES_API_KEY")
.expect("OPENFXRATES_API_KEY must be set");
}

2. Connection Pooling

Reuse the same reqwest::Client instance:

// Good: Single client instance
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.pool_max_idle_per_host(10)
.build()?;

// Use this client for all requests

3. Response Caching

Implement simple caching:

use std::collections::HashMap;
use std::sync::{Arc, Mutex};

struct CachedClient {
client: OpenFXRatesClient,
cache: Arc<Mutex<HashMap<String, (LatestRatesResponse, std::time::Instant)>>>,
cache_duration: Duration,
}

impl CachedClient {
pub async fn get_latest_rates_cached(
&self,
base: &str,
targets: Option<&str>,
) -> Result<LatestRatesResponse> {
let cache_key = format!("{}:{:?}", base, targets);

// Check cache
{
let cache = self.cache.lock().unwrap();
if let Some((data, timestamp)) = cache.get(&cache_key) {
if timestamp.elapsed() < self.cache_duration {
return Ok(data.clone());
}
}
}

// Fetch fresh data
let data = self.client.get_latest_rates(base, targets).await?;

// Update cache
{
let mut cache = self.cache.lock().unwrap();
cache.insert(cache_key, (data.clone(), std::time::Instant::now()));
}

Ok(data)
}
}

4. Type-Safe Configuration

use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct Config {
pub api_key: String,
pub base_url: Option<String>,
pub timeout_secs: Option<u64>,
}

impl Config {
pub fn from_env() -> Result<Self> {
dotenv::dotenv().ok();
Ok(Self {
api_key: std::env::var("OPENFXRATES_API_KEY")?,
base_url: std::env::var("OPENFXRATES_BASE_URL").ok(),
timeout_secs: std::env::var("OPENFXRATES_TIMEOUT")
.ok()
.and_then(|s| s.parse().ok()),
})
}
}

Add to Cargo.toml for error handling examples:

[dependencies]
thiserror = "1.0"