Als je fintech-infrastructuur, een pricing-engine, een CLI-tool of een backend met hoge doorvoer bouwt, heb je vroeg of laat live valutakoersgegevens nodig. Het aansluiten van een wisselkoers-API in Rust is een van de schoonste netwerktaken die de taal te bieden heeft: met reqwest, tokio en serde krijg je een asynchrone HTTP-client, automatische JSON-deserialisatie en garanties tijdens het compileren dat je code voor koersverwerking correct is nog voordat die ook maar draait. Deze handleiding loopt het hele pad af — van je eerste GET-verzoek tot een productieklare valutaomrekenaar met een herbruikbare client, getypeerde modellen, eigen foutafhandeling, decimaalveilige geldberekeningen en een in-memory cache die je binnen het gratis abonnement houdt.
Aan het eind heb je een klein, herbruikbaar Rust-module gebouwd op de Finexly API-documentatie, dat met realtime koersen omrekent tussen meer dan 170 valuta's. Als je onze Go-handleiding voor de valuta-API al hebt gelezen, is dit de Rust-tegenhanger met dezelfde architectuur — een getypeerde client, een cache en nette foutpropagatie.
Waarom Rust uitstekend past bij valutagegevens
Valutagegevens zijn I/O-gebonden, latentiegevoelig en — in elk systeem dat met geld te maken heeft — cruciaal wat correctheid betreft. Rust is ongewoon geschikt voor alle drie:
- Standaard asynchroon.
reqwestis gebouwd ophyperentokio, dus gelijktijdige koersophalingen zijn goedkoop en niet-blokkerend. Je kunt tientallen valutaparen parallel opvragen zonder OS-threads te starten. - Het typesysteem vangt fouten vroeg op. Een van
serdeafgeleide struct betekent dat een misvormd antwoord of een ontbrekend veld een fout is tijdens het compileren of deserialiseren, en niet een mysterieuzenulldrie lagen diep in productie. - Geen garbage collector, voorspelbare latentie. Voor een dienst die onder belasting FX-koersen levert, maakt het ontbreken van GC-pauzes uit.
- Decimaalveilig geld. Met de crate
rust_decimalkrijg je vastekomma-rekenkunde, zodat je nooit een afrondingsfout uitlevert die van € 100,00 opeens € 99,999999 maakt.
De afweging is een iets omslachtigere opzet dan bij een scripttaal, maar het resultaat is een valutaclient die je in een dienst kunt inbouwen en kunt vertrouwen.
Wat je gaat bouwen
Een herbruikbare FinexlyClient die:
- De nieuwste koersen voor een basisvaluta ophaalt van een wisselkoers-API.
- De JSON met
serdedeserialiseert naar getypeerde Rust-structs. - Omrekent tussen elke twee valuta's, zelfs wanneer geen van beide de basis is (kruiskoersen).
- Fouten afhandelt — netwerkstoringen, antwoorden anders dan 200, rate limits — met een eigen fouttype.
- Antwoorden in het geheugen cachet met een TTL zodat je ruim binnen een gratis abonnement blijft.
Vereisten en projectopzet
Je hebt een recente stabiele Rust-toolchain nodig (installeer via rustup) en een gratis Finexly API-sleutel. Je kunt je in minder dan een minuut gratis aanmelden — geen creditcard nodig, 1.000 verzoeken per maand in het gratis abonnement.
Maak een nieuw project aan en voeg de afhankelijkheden toe:
cargo new finexly-rates
cd finexly-rates
cargo add tokio --features full
cargo add reqwest --features json
cargo add serde --features derive
cargo add thiserror
cargo add rust_decimal rust_decimal_macrosDe afhankelijkheden in je Cargo.toml zouden er nu zo uit moeten zien:
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
thiserror = "2"
rust_decimal = "1"
rust_decimal_macros = "1"Een korte opmerking over de crates: reqwest is de de-facto asynchrone HTTP-client voor Rust; de json-feature haalt serde_json binnen en schakelt de .json()-helper in. serde met derive geeft je #[derive(Deserialize)]. thiserror maakt ergonomische eigen fout-enums mogelijk. rust_decimal levert vastekomma-decimalen voor geld.
Bewaar je sleutel in een omgevingsvariabele zodat die nooit in versiebeheer terechtkomt:
export FINEXLY_API_KEY="your_api_key_here"De vorm van het Finexly API-antwoord
Kijk voordat je code schrijft naar wat het endpoint teruggeeft. Een verzoek aan het endpoint voor de nieuwste koersen:
curl "https://api.finexly.com/v1/latest?base=USD&symbols=EUR,GBP,JPY&apikey=YOUR_KEY"geeft een klein, voorspelbaar JSON-object terug:
{
"success": true,
"base": "USD",
"timestamp": 1753660800,
"rates": {
"EUR": 0.9213,
"GBP": 0.7847,
"JPY": 161.42
}
}Twee dingen bepalen de code hieronder. Ten eerste is rates een platte map van een ISO 4217-valutacode naar een drijvendekommagetal — dat mapt netjes op een Rust-HashMap<String, f64>. Ten tweede vertelt base je waar die koersen relatief aan zijn. Om USD → EUR om te rekenen, vermenigvuldig je met rates["EUR"]. Om tussen twee niet-basisvaluta's om te rekenen, ga je via de basis: deel de ene eruit en vermenigvuldig de andere erin.
Je eerste verzoek maken met reqwest
Begin met het eenvoudigst mogelijke asynchrone verzoek. Vervang src/main.rs door:
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let api_key = std::env::var("FINEXLY_API_KEY")
.expect("Set FINEXLY_API_KEY in your environment");
let url = format!(
"https://api.finexly.com/v1/latest?base=USD&symbols=EUR,GBP,JPY&apikey={api_key}"
);
let body = reqwest::get(&url).await?.text().await?;
println!("{body}");
Ok(())
}Voer het uit met cargo run en je ziet de ruwe JSON afgedrukt. De macro #[tokio::main] zet de asynchrone runtime op, reqwest::get voert het verzoek uit, en elke .await? schort de taak op totdat het netwerk antwoordt, terwijl eventuele fouten worden doorgegeven. Dit werkt, maar reqwest::get maakt bij elke aanroep een gloednieuwe client aan — prima voor een eenmalig geval, verkeerd voor een echte applicatie. Dat lossen we zo op.
De JSON modelleren met Serde
Een string afdrukken is niet nuttig; je wilt getypeerde gegevens. Definieer structs die het antwoord weerspiegelen en laat serde het parsen doen:
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Deserialize)]
pub struct RatesResponse {
pub success: bool,
pub base: String,
pub timestamp: i64,
pub rates: HashMap<String, f64>,
}Wissel nu .text() in voor .json(), dat de body rechtstreeks in je struct deserialiseert:
let data: RatesResponse = reqwest::get(&url).await?.json().await?;
println!("1 USD = {} EUR", data.rates["EUR"]);De methode json() leest de antwoordbody en draait onder de motorkap serde_json. Als een veld ontbreekt of het verkeerde type heeft, krijg je een duidelijke deserialisatiefout in plaats van een stille null. Omdat rates een HashMap is, kun je elke teruggegeven valuta opzoeken via zijn ISO 4217-code.
Een herbruikbare client bouwen
Verreweg de meest voorkomende reqwest-fout is voor elk verzoek een nieuwe Client aanmaken. Elke client heeft zijn eigen connectiepool, dus opnieuw aanmaken gooit keep-alive-verbindingen en TLS-handshakes weg. Maak één Client, kloon hem goedkoop (het is intern een Arc) en hergebruik hem overal. Verpak hem in een kleine struct die de URL en de sleutel verbergt:
use reqwest::Client;
#[derive(Clone)]
pub struct FinexlyClient {
http: Client,
api_key: String,
base_url: String,
}
impl FinexlyClient {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
http: Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.expect("failed to build HTTP client"),
api_key: api_key.into(),
base_url: "https://api.finexly.com/v1".to_string(),
}
}
pub async fn latest(
&self,
base: &str,
symbols: &[&str],
) -> Result<RatesResponse, FinexlyError> {
let url = format!("{}/latest", self.base_url);
let symbols_csv = symbols.join(",");
let resp = self
.http
.get(&url)
.query(&[
("base", base),
("symbols", symbols_csv.as_str()),
("apikey", self.api_key.as_str()),
])
.send()
.await?;
let resp = resp.error_for_status()?;
let data = resp.json::<RatesResponse>().await?;
Ok(data)
}
}Een paar dingen zijn het vermelden waard. De builder .query(&[...]) verzorgt de URL-codering voor je, zodat je nooit handmatig querystrings aan elkaar plakt. De .timeout(...) op de builder beschermt tegen een vastgelopen verbinding die je dienst laat stilvallen. En error_for_status() verandert elk HTTP-antwoord dat niet 2xx is in een Err — wat ons bij de foutafhandeling brengt.
Robuuste foutafhandeling
Productiecode moet antwoord geven: wat gebeurt er als het netwerk plat ligt, de sleutel ongeldig is (403) of je de rate limit bereikt (429)? Modelleer die gevallen met een eigen fout-enum met behulp van thiserror:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FinexlyError {
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("API returned an unsuccessful response")]
Unsuccessful,
#[error("currency '{0}' was not present in the response")]
MissingCurrency(String),
}De regel #[from] reqwest::Error betekent dat elke reqwest-storing — DNS, TLS, timeout of een niet-2xx-status opgevangen door error_for_status() — automatisch via de ?-operator wordt omgezet in FinexlyError::Http. Daarom werkt elke .await? in latest() gewoon.
Je kunt de statuscode ook expliciet inspecteren wanneer je op maat gemaakt gedrag wilt — bijvoorbeeld terugvallen op een backoff bij een 429:
if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
// sleep and retry, or fall back to a cached rate
}Voor een diepere behandeling van retries, backoff en cachingstrategie, zie onze gids over best practices voor caching en foutafhandeling bij valuta-API's.
Omrekenen tussen elke twee valuta's
Vermenigvuldigen met één enkele koers werkt alleen wanneer je bronvaluta de basis is. Echte applicaties hebben willekeurige paren nodig — EUR → JPY, GBP → CAD — waarbij geen van beide kanten de basis is. De truc is om via de basisvaluta te gaan: deel het bedrag door de bronkoers om het basis-equivalent te krijgen, en vermenigvuldig vervolgens met de doelkoers.
impl FinexlyClient {
/// Convert `amount` from `from` to `to`, using `from` as the request base.
pub async fn convert(
&self,
amount: f64,
from: &str,
to: &str,
) -> Result<f64, FinexlyError> {
let data = self.latest(from, &[to]).await?;
if !data.success {
return Err(FinexlyError::Unsuccessful);
}
let rate = data
.rates
.get(to)
.ok_or_else(|| FinexlyError::MissingCurrency(to.to_string()))?;
Ok(amount * rate)
}
}Hier vragen we simpelweg de koersen op met from als basis, zodat de teruggegeven koers al de directe from → to-omrekening is. Als je één basisvaluta cachet (bijvoorbeeld USD) en kruiskoersen nodig hebt, bereken die dan in plaats daarvan aan de clientzijde: amount / rates[from] * rates[to]. Probeer het van begin tot eind:
#[tokio::main]
async fn main() -> Result<(), FinexlyError> {
let client = FinexlyClient::new(
std::env::var("FINEXLY_API_KEY").expect("set FINEXLY_API_KEY"),
);
let usd = client.convert(250.0, "EUR", "USD").await?;
println!("250 EUR = {usd:.2} USD");
Ok(())
}rust_decimal gebruiken voor geld
f64 is prima voor weergaveomrekeningen, maar het is het verkeerde type voor het opslaan van saldi, facturering of alles wat tot op de cent moet kloppen. Binaire drijvende komma kan 0.1 niet exact weergeven, en die fouten stapelen zich op. Gebruik voor geld vastekomma-decimalen:
use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive;
use rust_decimal_macros::dec;
pub fn convert_decimal(amount: Decimal, rate: f64) -> Decimal {
let rate = Decimal::from_f64(rate).unwrap_or_default();
(amount * rate).round_dp(2)
}
// usage
let total = convert_decimal(dec!(1999.99), 0.9213);
println!("{total}"); // rounded to 2 decimal placesEen belangrijke kanttekening: niet elke valuta heeft twee onderverdelingen. JPY en KRW hebben nul decimalen; sommige valuta's zoals BHD en KWD hebben er drie. Rond af op het juiste aantal decimalen per valuta (gestuurd door de ISO 4217-tabel met onderverdelingen) in plaats van 2 hard te coderen. Onze valutaomrekenaar en de API-documentatie weerspiegelen beide deze per-valutaregels.
Cachen om binnen het gratis abonnement te blijven
Wisselkoersen bewegen niet noemenswaardig van seconde tot seconde. Voor de meeste weergave-, checkout- en rapportagetoepassingen is één keer per uur vernieuwen ruim voldoende — en cachen houdt je comfortabel binnen een gratis abonnement, hoeveel verkeer je ook krijgt. Voeg een eenvoudige in-memory TTL-cache toe die beschermd wordt door een Mutex:
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
struct CacheEntry {
data: RatesResponse,
fetched_at: Instant,
}
pub struct CachedClient {
inner: FinexlyClient,
ttl: Duration,
cache: Mutex<HashMap<String, CacheEntry>>,
}
impl CachedClient {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
inner: FinexlyClient::new(api_key),
ttl: Duration::from_secs(3600), // 1 hour
cache: Mutex::new(HashMap::new()),
}
}
pub async fn latest(&self, base: &str) -> Result<RatesResponse, FinexlyError> {
{
let cache = self.cache.lock().unwrap();
if let Some(entry) = cache.get(base) {
if entry.fetched_at.elapsed() < self.ttl {
return Ok(entry.data.clone());
}
}
}
let fresh = self.inner.latest(base, &[]).await?;
let mut cache = self.cache.lock().unwrap();
cache.insert(
base.to_string(),
CacheEntry { data: fresh.clone(), fetched_at: Instant::now() },
);
Ok(fresh)
}
}Om dit te laten compileren wil je #[derive(Clone)] op RatesResponse hebben. Met een TTL van een uur kost een enkele basisvaluta je hooguit 24 verzoeken per dag, ongeacht hoeveel omrekeningen je gebruikers uitvoeren — een afrondingsfout tegenover een gratis abonnement met 1.000 verzoeken. Als je erboven uitgroeit, schalen de prijsplannen lineair. Voor patronen als stale-while-revalidate en gedeelde caches over meerdere instanties, zie onze gids over caching en foutafhandeling.
Historische koersen ophalen
Terugwerkende facturen, rapportages en grafieken hebben historische gegevens nodig. Finexly biedt een historisch endpoint dat je vanaf dezelfde client kunt aanroepen door een methode toe te voegen die /v1/historical aanspreekt:
impl FinexlyClient {
pub async fn historical(
&self,
date: &str, // "YYYY-MM-DD"
base: &str,
symbols: &[&str],
) -> Result<RatesResponse, FinexlyError> {
let url = format!("{}/historical", self.base_url);
let symbols_csv = symbols.join(",");
let data = self
.http
.get(&url)
.query(&[
("date", date),
("base", base),
("symbols", symbols_csv.as_str()),
("apikey", self.api_key.as_str()),
])
.send()
.await?
.error_for_status()?
.json::<RatesResponse>()
.await?;
Ok(data)
}
}De vorm van het antwoord is identiek aan die van het endpoint voor de nieuwste koersen, dus al je bestaande parse- en omrekeningscode werkt gewoon. Zie de API-documentatie voor de volledige parameterlijst en het endpoint /v1/timeseries voor datumbereiken.
Veelvoorkomende valkuilen om te vermijden
- Een
Clientper verzoek aanmaken. Bouw er één, kloon hem (dat is goedkoop) en hergebruik hem zodat de connectiepool warm blijft. f64gebruiken voor opgeslagen saldi. Prima voor weergave, verkeerd voor grootboeken — gebruikrust_decimalvoor alles wat moet kloppen.- Twee decimalen hard coderen. JPY en KRW hebben er nul; een paar valuta's hebben er drie. Rond af per valuta met de ISO 4217-onderverdelingen.
- De statuscontrole overslaan. Een
403- of429-body als koersgegevens deserialiseren levert verwarrende bugs op.error_for_status()handelt dit af in één regel. - Bij elke omrekening ophalen. Cache met een TTL en debounce gebruikersinvoer zodat je je quota niet opbrandt.
- Je API-sleutel committen. Lees hem uit de omgeving of een secrets manager, nooit uit een stringliteral in
main.rs.
Als je nog aanbieders vergelijkt, ontleedt onze vergelijking van valuta-API's nauwkeurigheid, valutadekking en limieten van het gratis abonnement, en de gids voor gratis valuta-API's behandelt de kosteloze opties uitgebreider.
Veelgestelde vragen
Wat is de beste HTTP-client om in Rust een valuta-API aan te roepen?
reqwest is de standaardkeuze voor asynchroon Rust. Het is gebouwd op hyper en tokio, verzorgt connection pooling, TLS en JSON out of the box, en gaat van nature samen met serde voor getypeerde antwoorden. Schakel de json-feature in en hergebruik één enkele Client-instantie in je hele applicatie.Hoe parse ik in Rust een JSON-antwoord met wisselkoersen?
Definieer een struct die het antwoord weerspiegelt en leid erserde::Deserialize op af. Modelleer het veld rates als een HashMap<String, f64> aangezien het een platte map is van valutacode naar koers, roep vervolgens .json::<RatesResponse>() aan op het antwoord en reqwest deserialiseert het voor je.Moet ik f64 of Decimal gebruiken voor valutaomrekening in Rust?
Gebruikf64 voor snelle weergaveomrekeningen, maar gebruik rust_decimal::Decimal voor alles wat je opslaat of afstemt — saldi, facturen, grootboeken. Binaire drijvende komma kan decimale breuken niet exact weergeven, dus afrondingsfouten stapelen zich op. rust_decimal geeft je vastekomma-precisie.Hoe vaak moet ik wisselkoersen ophalen in een Rust-dienst?
Voor de meeste checkout-, weergave- en rapportagetoepassingen is één keer per uur ruim voldoende — koersen bewegen binnen een uur niet genoeg om ertoe te doen. Cache het antwoord met een TTL van een uur, wat je op ongeveer 24 aanroepen per basisvaluta per dag houdt en comfortabel binnen een gratis abonnement.Kan ik in Rust historische wisselkoersen krijgen?
Ja. Voeg een methode toe die het endpoint/v1/historical?date=YYYY-MM-DD op dezelfde client aanroept. De JSON-vorm komt overeen met die van het endpoint voor de nieuwste koersen, dus je bestaande structs en omrekeningslogica werken ongewijzigd. Dit is nuttig voor terugwerkende facturen, rapportages en grafieken.Begin met bouwen
Klaar om realtime wisselkoersen in je Rust-project te integreren? Haal je gratis Finexly API-sleutel — geen creditcard nodig. Begin met 1.000 gratis verzoeken per maand en upgrade naarmate je groeit. Bekijk de API-documentatie voor de volledige endpointreferentie en verken de prijsplannen wanneer je klaar bent om je multivalutafuncties op te schalen.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →