Wenn Sie eine Fintech-Infrastruktur, eine Pricing-Engine, ein CLI-Tool oder ein Backend mit hohem Durchsatz aufbauen, werden Sie früher oder später Live-Devisendaten benötigen. Eine Wechselkurs-API in Rust anzubinden gehört zu den saubersten Netzwerkaufgaben, die die Sprache zu bieten hat: Mit reqwest, tokio und serde erhalten Sie einen asynchronen HTTP-Client, automatische JSON-Deserialisierung und Garantien zur Kompilierzeit, dass Ihr Code zur Kursverarbeitung korrekt ist, noch bevor er überhaupt läuft. Dieses Tutorial führt Sie den gesamten Weg entlang — von Ihrer ersten GET-Anfrage bis zu einem produktionsreifen Währungsrechner mit einem wiederverwendbaren Client, typisierten Modellen, eigener Fehlerbehandlung, dezimalsicherer Geldarithmetik und einem In-Memory-Cache, der Sie innerhalb des kostenlosen Tarifs hält.
Am Ende verfügen Sie über ein kleines, wiederverwendbares Rust-Modul auf Basis der Finexly-API-Dokumentation, das mit Echtzeitkursen zwischen beliebigen von über 170 Währungen umrechnet. Wenn Sie bereits unser Go-Tutorial zur Währungs-API gelesen haben, ist dies das Rust-Gegenstück mit derselben Architektur — ein typisierter Client, ein Cache und eine saubere Fehlerweitergabe.
Warum sich Rust hervorragend für Währungsdaten eignet
Währungsdaten sind I/O-gebunden, latenzempfindlich und — in jedem System, das mit Geld zu tun hat — kritisch, was die Korrektheit angeht. Rust ist für alle drei Punkte ungewöhnlich gut geeignet:
- Asynchron von Haus aus.
reqwestbaut aufhyperundtokioauf, sodass gleichzeitige Kursabrufe günstig und nicht blockierend sind. Sie können Dutzende von Währungspaaren parallel anfragen, ohne Betriebssystem-Threads zu erzeugen. - Das Typsystem erkennt Fehler früh. Ein von
serdeabgeleitetes Struct bedeutet, dass eine fehlerhafte Antwort oder ein fehlendes Feld ein Fehler zur Kompilier- oder Deserialisierungszeit ist und kein mysteriösesnulldrei Ebenen tief in der Produktion. - Kein Garbage Collector, vorhersehbare Latenz. Für einen Dienst, der unter Last FX-Kurse liefert, ist das Ausbleiben von GC-Pausen entscheidend.
- Dezimalsicheres Geld. Mit der Crate
rust_decimalerhalten Sie Festkomma-Arithmetik, sodass Sie nie einen Rundungsfehler ausliefern, der aus 100,00 € plötzlich 99,999999 € macht.
Der Kompromiss ist eine etwas aufwendigere Einrichtung als bei einer Skriptsprache, aber das Ergebnis ist ein Währungsclient, den Sie in einen Dienst einbauen und dem Sie vertrauen können.
Was Sie bauen werden
Einen wiederverwendbaren FinexlyClient, der:
- Die aktuellsten Kurse für eine Basiswährung von einer Wechselkurs-API abruft.
- Das JSON mit
serdein typisierte Rust-Structs deserialisiert. - Zwischen beliebigen zwei Währungen umrechnet, selbst wenn keine davon die Basis ist (Kreuzkurse).
- Fehler behandelt — Netzwerkausfälle, Antworten ungleich 200, Rate-Limits — mit einem eigenen Fehlertyp.
- Antworten mit einer TTL im Arbeitsspeicher zwischenspeichert, damit Sie komfortabel innerhalb eines kostenlosen Tarifs bleiben.
Voraussetzungen und Projekteinrichtung
Sie benötigen eine aktuelle stabile Rust-Toolchain (Installation über rustup) und einen kostenlosen Finexly-API-Schlüssel. Sie können sich in unter einer Minute kostenlos registrieren — keine Kreditkarte erforderlich, 1.000 Anfragen pro Monat im kostenlosen Tarif.
Erstellen Sie ein neues Projekt und fügen Sie die Abhängigkeiten hinzu:
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_macrosIhre Cargo.toml-Abhängigkeiten sollten nun so aussehen:
[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"Ein kurzer Hinweis zu den Crates: reqwest ist der De-facto-Standard für asynchrone HTTP-Clients in Rust; das json-Feature zieht serde_json mit ein und aktiviert den .json()-Helfer. serde mit derive gibt Ihnen #[derive(Deserialize)]. thiserror ermöglicht ergonomische eigene Fehler-Enums. rust_decimal stellt Festkomma-Dezimalzahlen für Geld bereit.
Speichern Sie Ihren Schlüssel in einer Umgebungsvariablen, damit er nie in die Versionskontrolle gelangt:
export FINEXLY_API_KEY="your_api_key_here"Die Form der Finexly-API-Antwort
Bevor Sie Code schreiben, schauen Sie sich an, was der Endpunkt zurückgibt. Eine Anfrage an den Endpunkt für die aktuellsten Kurse:
curl "https://api.finexly.com/v1/latest?base=USD&symbols=EUR,GBP,JPY&apikey=YOUR_KEY"liefert ein kleines, vorhersehbares JSON-Objekt zurück:
{
"success": true,
"base": "USD",
"timestamp": 1753660800,
"rates": {
"EUR": 0.9213,
"GBP": 0.7847,
"JPY": 161.42
}
}Zwei Dinge prägen den nachfolgenden Code. Erstens ist rates eine flache Map von ISO-4217-Währungscode zu einer Gleitkommazahl — das bildet sich sauber auf eine Rust-HashMap<String, f64> ab. Zweitens sagt Ihnen base, worauf sich diese Kurse beziehen. Um USD → EUR umzurechnen, multiplizieren Sie mit rates["EUR"]. Um zwischen zwei Nicht-Basiswährungen umzurechnen, gehen Sie über die Basis: Teilen Sie die eine heraus und multiplizieren Sie die andere hinein.
Ihre erste Anfrage mit reqwest
Beginnen Sie mit der einfachstmöglichen asynchronen Anfrage. Ersetzen Sie src/main.rs durch:
#[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(())
}Führen Sie es mit cargo run aus, und Sie sehen das rohe JSON ausgegeben. Das Makro #[tokio::main] richtet die asynchrone Laufzeitumgebung ein, reqwest::get führt die Anfrage aus, und jedes .await? pausiert die Task, bis das Netzwerk antwortet, während es einen etwaigen Fehler weitergibt. Das funktioniert, aber reqwest::get erstellt bei jedem Aufruf einen brandneuen Client — in Ordnung für einen einmaligen Fall, falsch für eine echte Anwendung. Das beheben wir gleich.
Das JSON mit Serde modellieren
Eine Zeichenkette auszugeben ist nicht nützlich; Sie wollen typisierte Daten. Definieren Sie Structs, die die Antwort abbilden, und lassen Sie serde das Parsen übernehmen:
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>,
}Tauschen Sie nun .text() gegen .json() aus, das den Body direkt in Ihr Struct deserialisiert:
let data: RatesResponse = reqwest::get(&url).await?.json().await?;
println!("1 USD = {} EUR", data.rates["EUR"]);Die Methode json() liest den Antwort-Body und führt im Hintergrund serde_json aus. Wenn ein Feld fehlt oder den falschen Typ hat, erhalten Sie einen klaren Deserialisierungsfehler statt eines stillen null. Da rates eine HashMap ist, können Sie jede zurückgegebene Währung über ihren ISO-4217-Code nachschlagen.
Einen wiederverwendbaren Client bauen
Der mit Abstand häufigste reqwest-Fehler ist, für jede Anfrage einen neuen Client zu erstellen. Jeder Client besitzt seinen eigenen Verbindungspool, sodass ein Neuerstellen Keep-alive-Verbindungen und TLS-Handshakes verwirft. Erstellen Sie einen Client, klonen Sie ihn günstig (er ist intern ein Arc) und verwenden Sie ihn überall wieder. Verpacken Sie ihn in ein kleines Struct, das die URL und den Schlüssel verbirgt:
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)
}
}Ein paar Dinge sind hervorzuheben. Der Builder .query(&[...]) übernimmt für Sie die URL-Kodierung, sodass Sie nie manuell Query-Strings zusammensetzen. Der .timeout(...) am Builder schützt davor, dass eine hängende Verbindung Ihren Dienst blockiert. Und error_for_status() verwandelt jede HTTP-Antwort ungleich 2xx in ein Err — was uns zur Fehlerbehandlung bringt.
Robuste Fehlerbehandlung
Produktionscode muss beantworten: Was passiert, wenn das Netzwerk ausfällt, der Schlüssel ungültig ist (403) oder Sie das Rate-Limit erreichen (429)? Modellieren Sie diese Fälle mit einem eigenen Fehler-Enum unter Verwendung von 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),
}Die Zeile #[from] reqwest::Error bedeutet, dass jeder reqwest-Fehler — DNS, TLS, Timeout oder ein von error_for_status() erfasster Status ungleich 2xx — automatisch über den ?-Operator in FinexlyError::Http umgewandelt wird. Deshalb funktioniert jedes .await? in latest() einfach so.
Sie können den Statuscode auch explizit prüfen, wenn Sie ein maßgeschneidertes Verhalten wünschen — zum Beispiel ein Backoff bei einem 429:
if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
// sleep and retry, or fall back to a cached rate
}Eine ausführlichere Behandlung von Retries, Backoff und Caching-Strategie finden Sie in unserem Leitfaden zu Best Practices für Caching und Fehlerbehandlung bei Währungs-APIs.
Zwischen beliebigen zwei Währungen umrechnen
Die Multiplikation mit einem einzigen Kurs funktioniert nur, wenn Ihre Ausgangswährung die Basis ist. Echte Anwendungen benötigen beliebige Paare — EUR → JPY, GBP → CAD — bei denen keine Seite die Basis ist. Der Trick besteht darin, über die Basiswährung zu leiten: Teilen Sie den Betrag durch den Ausgangskurs, um das Basis-Äquivalent zu erhalten, und multiplizieren Sie dann mit dem Zielkurs.
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 fragen wir einfach die Kurse mit from als Basis an, sodass der zurückgegebene Kurs bereits die direkte Umrechnung from → to ist. Wenn Sie eine Basiswährung (etwa USD) zwischenspeichern und Kreuzkurse benötigen, berechnen Sie diese stattdessen clientseitig: amount / rates[from] * rates[to]. Probieren Sie es von Anfang bis Ende aus:
#[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 für Geld verwenden
f64 ist für Anzeigeumrechnungen in Ordnung, aber es ist der falsche Typ zum Speichern von Salden, für die Rechnungsstellung oder für alles, das auf den Cent genau abgestimmt werden muss. Binäre Gleitkommazahlen können 0.1 nicht exakt darstellen, und diese Fehler summieren sich. Verwenden Sie für Geld Festkomma-Dezimalzahlen:
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 placesEin wichtiger Vorbehalt: Nicht jede Währung hat zwei Nebeneinheiten. JPY und KRW haben null Dezimalstellen; einige Währungen wie BHD und KWD haben drei. Runden Sie pro Währung auf die korrekte Anzahl an Dezimalstellen (gesteuert durch die ISO-4217-Tabelle der Nebeneinheiten), statt 2 fest zu codieren. Unser Währungsrechner und die API-Dokumentation spiegeln beide diese währungsspezifischen Regeln wider.
Caching, um innerhalb des kostenlosen Tarifs zu bleiben
Wechselkurse bewegen sich nicht von Sekunde zu Sekunde nennenswert. Für die meisten Anzeige-, Checkout- und Reporting-Anwendungsfälle ist eine Aktualisierung einmal pro Stunde mehr als genug — und Caching hält Sie bequem innerhalb eines kostenlosen Tarifs, egal wie viel Traffic Sie erhalten. Fügen Sie einen einfachen In-Memory-TTL-Cache hinzu, der durch einen Mutex geschützt ist:
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)
}
}Damit dies kompiliert, sollten Sie #[derive(Clone)] auf RatesResponse setzen. Mit einer TTL von einer Stunde kostet Sie eine einzelne Basiswährung höchstens 24 Anfragen pro Tag, unabhängig davon, wie viele Umrechnungen Ihre Nutzer durchführen — ein Rundungsfehler gegenüber einem kostenlosen Tarif mit 1.000 Anfragen. Wenn Sie darüber hinauswachsen, skalieren die Preispläne linear. Für Muster wie Stale-while-revalidate und gemeinsam genutzte Caches über mehrere Instanzen hinweg lesen Sie unseren Leitfaden zu Caching und Fehlerbehandlung.
Historische Kurse abrufen
Rückdatierte Rechnungen, Reporting und Diagramme benötigen historische Daten. Finexly stellt einen historischen Endpunkt bereit, den Sie vom selben Client aus aufrufen können, indem Sie eine Methode hinzufügen, die /v1/historical anspricht:
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)
}
}Die Form der Antwort ist mit der des Latest-Endpunkts identisch, sodass Ihr gesamter vorhandener Parsing- und Umrechnungscode einfach funktioniert. Die vollständige Parameterliste und den Endpunkt /v1/timeseries für Datumsbereiche finden Sie in der API-Dokumentation.
Häufige Fallstricke, die es zu vermeiden gilt
- Einen
Clientpro Anfrage erstellen. Bauen Sie einen, klonen Sie ihn (das ist günstig) und verwenden Sie ihn wieder, damit der Verbindungspool warm bleibt. f64für gespeicherte Salden verwenden. Für die Anzeige in Ordnung, für Buchungssysteme falsch — verwenden Sierust_decimalfür alles, das abgestimmt werden muss.- Zwei Dezimalstellen fest codieren. JPY und KRW haben null; einige wenige Währungen haben drei. Runden Sie pro Währung anhand der ISO-4217-Nebeneinheiten.
- Die Statusprüfung überspringen. Einen
403- oder429-Body als Kursdaten zu deserialisieren führt zu verwirrenden Fehlern.error_for_status()erledigt das in einer Zeile. - Bei jeder Umrechnung abrufen. Cachen Sie mit einer TTL und entprellen Sie Nutzereingaben, damit Sie Ihr Kontingent nicht verbrauchen.
- Ihren API-Schlüssel committen. Lesen Sie ihn aus der Umgebung oder einem Secrets-Manager, niemals aus einem String-Literal in
main.rs.
Wenn Sie noch Anbieter vergleichen, schlüsselt unser Vergleich von Währungs-APIs Genauigkeit, Währungsabdeckung und Free-Tier-Limits auf, und der Leitfaden zu kostenlosen Währungs-APIs behandelt die kostenfreien Optionen ausführlicher.
Häufig gestellte Fragen
Was ist der beste HTTP-Client, um in Rust eine Währungs-API aufzurufen?
reqwest ist die Standardwahl für asynchrones Rust. Es baut auf hyper und tokio auf, übernimmt Connection-Pooling, TLS und JSON von Haus aus und passt natürlich mit serde für typisierte Antworten zusammen. Aktivieren Sie das json-Feature und verwenden Sie eine einzelne Client-Instanz in Ihrer gesamten Anwendung wieder.Wie parse ich in Rust eine JSON-Antwort mit Wechselkursen?
Definieren Sie ein Struct, das die Antwort abbildet, und leiten Sieserde::Deserialize darauf ab. Modellieren Sie das Feld rates als HashMap<String, f64>, da es eine flache Map von Währungscode zu Kurs ist, rufen Sie dann .json::<RatesResponse>() auf der Antwort auf, und reqwest deserialisiert es für Sie.Sollte ich für die Währungsumrechnung in Rust f64 oder Decimal verwenden?
Verwenden Sief64 für schnelle Anzeigeumrechnungen, aber nutzen Sie rust_decimal::Decimal für alles, das Sie speichern oder abstimmen — Salden, Rechnungen, Buchungssysteme. Binäre Gleitkommazahlen können Dezimalbrüche nicht exakt darstellen, sodass sich Rundungsfehler ansammeln. rust_decimal bietet Ihnen Festkomma-Präzision.Wie oft sollte ich in einem Rust-Dienst Wechselkurse abrufen?
Für die meisten Checkout-, Anzeige- und Reporting-Anwendungsfälle ist einmal pro Stunde mehr als genug — Kurse bewegen sich innerhalb einer Stunde nicht genug, um ins Gewicht zu fallen. Cachen Sie die Antwort mit einer TTL von einer Stunde, was Sie auf etwa 24 Aufrufe pro Basiswährung und Tag hält und komfortabel innerhalb eines kostenlosen Tarifs bleibt.Kann ich in Rust historische Wechselkurse abrufen?
Ja. Fügen Sie eine Methode hinzu, die den Endpunkt/v1/historical?date=YYYY-MM-DD auf demselben Client aufruft. Die JSON-Form entspricht der des Latest-Endpunkts, sodass Ihre vorhandenen Structs und Ihre Umrechnungslogik unverändert funktionieren. Das ist nützlich für rückdatierte Rechnungen, Reporting und Diagramme.Loslegen
Bereit, Echtzeit-Wechselkurse in Ihr Rust-Projekt zu integrieren? Holen Sie sich Ihren kostenlosen Finexly-API-Schlüssel — keine Kreditkarte erforderlich. Beginnen Sie mit 1.000 kostenlosen Anfragen pro Monat und führen Sie ein Upgrade durch, während Sie wachsen. Werfen Sie einen Blick in die API-Dokumentation für die vollständige Endpunktreferenz und erkunden Sie die Preispläne, wenn Sie bereit sind, Ihre Multi-Währungs-Funktionen zu skalieren.
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 →