Si vous construisez une infrastructure fintech, un moteur de tarification, un outil en ligne de commande ou un backend à haut débit, tôt ou tard vous aurez besoin de données de change en temps réel. Brancher une API de taux de change de devises en Rust est l'une des tâches réseau les plus propres qu'offre le langage : avec reqwest, tokio et serde, vous obtenez un client HTTP asynchrone, la désérialisation automatique du JSON et des garanties à la compilation que votre code de gestion des taux est correct avant même son exécution. Ce tutoriel parcourt tout le chemin — de votre première requête GET jusqu'à un convertisseur de devises prêt pour la production, avec un client réutilisable, des modèles typés, une gestion d'erreurs personnalisée, des calculs monétaires sûrs en décimal et un cache en mémoire qui vous maintient dans les limites de l'offre gratuite.
À la fin, vous disposerez d'un petit module Rust réutilisable, bâti sur la documentation de l'API Finexly, qui convertit entre plus de 170 devises avec des taux en temps réel. Si vous avez déjà lu notre tutoriel de l'API de devises en Go, voici son équivalent en Rust avec la même architecture — un client typé, un cache et une propagation d'erreurs propre.
Pourquoi Rust convient si bien aux données de devises
Les données de devises sont limitées par les E/S, sensibles à la latence et — dans tout système qui touche à l'argent — critiques quant à l'exactitude. Rust est particulièrement bien adapté à ces trois exigences :
- Asynchrone par défaut.
reqwestest bâti surhyperettokio, si bien que les récupérations de taux concurrentes sont peu coûteuses et non bloquantes. Vous pouvez demander des dizaines de paires de devises en parallèle sans créer de threads système. - Le système de types détecte les erreurs tôt. Une struct dérivée avec
serdefait qu'une réponse malformée ou un champ manquant devient une erreur à la compilation ou à la désérialisation, et non un mystérieuxnulltrois couches plus bas en production. - Pas de ramasse-miettes, latence prévisible. Pour un service qui cote des taux de change sous charge, l'absence de pauses du GC compte.
- Argent sûr en décimal. Avec le crate
rust_decimal, vous obtenez une arithmétique à virgule fixe, si bien que vous n'expédiez jamais un bug d'arrondi qui transforme 100,00 € en 99,999999 €.
Le compromis est une mise en place un peu plus lourde que dans un langage de script, mais le résultat est un client de devises que vous pouvez intégrer à un service et auquel vous pouvez vous fier.
Ce que vous allez construire
Un FinexlyClient réutilisable qui :
- Récupère les derniers taux pour une devise de base depuis une API de taux de change de devises.
- Désérialise le JSON en structs Rust typées avec
serde. - Convertit entre deux devises quelconques, même lorsque aucune n'est la base (taux croisés).
- Gère les erreurs — pannes réseau, réponses différentes de 200, limites de débit — avec un type d'erreur personnalisé.
- Met les réponses en cache en mémoire avec un TTL pour que vous restiez confortablement dans les limites d'une offre gratuite.
Prérequis et configuration du projet
Vous avez besoin d'une chaîne d'outils Rust stable et récente (installez-la via rustup) et d'une clé gratuite de l'API Finexly. Vous pouvez vous inscrire gratuitement en moins d'une minute — sans carte bancaire, 1 000 requêtes par mois sur l'offre gratuite.
Créez un nouveau projet et ajoutez les dépendances :
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_macrosLes dépendances de votre Cargo.toml devraient maintenant ressembler à ceci :
[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"Une remarque rapide sur les crates : reqwest est le client HTTP asynchrone de fait pour Rust ; la fonctionnalité json intègre serde_json et active l'assistant .json(). serde avec derive vous donne #[derive(Deserialize)]. thiserror facilite la création d'énumérations d'erreurs personnalisées et ergonomiques. rust_decimal fournit des décimaux à virgule fixe pour l'argent.
Stockez votre clé dans une variable d'environnement pour qu'elle n'atterrisse jamais dans le contrôle de version :
export FINEXLY_API_KEY="your_api_key_here"La forme de la réponse de l'API Finexly
Avant d'écrire la moindre ligne de code, observez ce que renvoie le endpoint. Une requête vers le endpoint des derniers taux :
curl "https://api.finexly.com/v1/latest?base=USD&symbols=EUR,GBP,JPY&apikey=YOUR_KEY"renvoie un petit objet JSON prévisible :
{
"success": true,
"base": "USD",
"timestamp": 1753660800,
"rates": {
"EUR": 0.9213,
"GBP": 0.7847,
"JPY": 161.42
}
}Deux éléments façonnent le code ci-dessous. Premièrement, rates est une map plate d'un code de devise ISO 4217 vers un nombre à virgule flottante — ce qui correspond proprement à un HashMap<String, f64> de Rust. Deuxièmement, base vous indique par rapport à quoi ces taux sont exprimés. Pour convertir USD → EUR, vous multipliez par rates["EUR"]. Pour convertir entre deux devises qui ne sont pas la base, vous passez par la base : vous divisez par l'une et multipliez par l'autre.
Faire votre première requête avec reqwest
Commencez par la requête asynchrone la plus simple possible. Remplacez src/main.rs par :
#[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(())
}Exécutez-le avec cargo run et vous verrez le JSON brut s'afficher. La macro #[tokio::main] met en place le runtime asynchrone, reqwest::get effectue la requête, et chaque .await? suspend la tâche jusqu'à ce que le réseau réponde, tout en propageant toute erreur. Cela fonctionne, mais reqwest::get crée un client flambant neuf à chaque appel — acceptable pour un cas isolé, mais inadapté à une véritable application. Nous corrigerons cela sous peu.
Modéliser le JSON avec Serde
Afficher une chaîne n'est pas utile ; vous voulez des données typées. Définissez des structs qui reflètent la réponse et laissez serde faire l'analyse :
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>,
}Maintenant, remplacez .text() par .json(), qui désérialise le corps directement dans votre struct :
let data: RatesResponse = reqwest::get(&url).await?.json().await?;
println!("1 USD = {} EUR", data.rates["EUR"]);La méthode json() lit le corps de la réponse et exécute serde_json en coulisses. Si un champ est manquant ou du mauvais type, vous obtenez une erreur de désérialisation claire au lieu d'un null silencieux. Comme rates est un HashMap, vous pouvez rechercher n'importe quelle devise renvoyée par son code ISO 4217.
Construire un client réutilisable
L'erreur la plus courante avec reqwest est de créer un nouveau Client pour chaque requête. Chaque client possède son propre pool de connexions, si bien que le recréer jette les connexions keep-alive et les poignées de main TLS. Créez un seul Client, clonez-le à faible coût (c'est un Arc en interne) et réutilisez-le partout. Enveloppez-le dans une petite struct qui masque l'URL et la clé :
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)
}
}Quelques points méritent d'être soulignés. Le constructeur .query(&[...]) se charge de l'encodage de l'URL à votre place, si bien que vous ne concaténez jamais manuellement de chaînes de requête. Le .timeout(...) du builder protège contre une connexion bloquée qui figerait votre service. Et error_for_status() transforme toute réponse HTTP différente de 2xx en Err — ce qui nous amène à la gestion des erreurs.
Gestion robuste des erreurs
Le code de production doit répondre : que se passe-t-il lorsque le réseau est en panne, que la clé est invalide (403) ou que vous atteignez la limite de débit (429) ? Modélisez ces cas avec une énumération d'erreurs personnalisée à l'aide de 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),
}La ligne #[from] reqwest::Error signifie que toute défaillance de reqwest — DNS, TLS, timeout ou un statut différent de 2xx capturé par error_for_status() — est automatiquement convertie en FinexlyError::Http via l'opérateur ?. C'est pourquoi chaque .await? dans latest() fonctionne tout simplement.
Vous pouvez aussi inspecter le code de statut explicitement lorsque vous souhaitez un comportement sur mesure — par exemple, appliquer un backoff face à un 429 :
if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
// sleep and retry, or fall back to a cached rate
}Pour un traitement plus approfondi des nouvelles tentatives, du backoff et de la stratégie de cache, consultez notre guide des bonnes pratiques de cache et de gestion des erreurs pour les API de devises.
Convertir entre deux devises quelconques
Multiplier par un seul taux ne fonctionne que lorsque votre devise source est la base. Les applications réelles ont besoin de paires arbitraires — EUR → JPY, GBP → CAD — où aucun des deux côtés n'est la base. L'astuce consiste à passer par la devise de base : divisez le montant par le taux source pour obtenir l'équivalent en base, puis multipliez par le taux cible.
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)
}
}Ici, nous demandons simplement les taux avec from comme base, si bien que le taux renvoyé est déjà la conversion directe from → to. Si vous mettez en cache une devise de base (disons USD) et que vous avez besoin de taux croisés, calculez-les plutôt côté client : amount / rates[from] * rates[to]. Essayez de bout en bout :
#[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(())
}Utiliser rust_decimal pour l'argent
f64 convient pour les conversions d'affichage, mais c'est le mauvais type pour stocker des soldes, facturer ou tout ce qui doit se rapprocher au centime près. La virgule flottante binaire ne peut pas représenter 0.1 exactement, et ces erreurs s'accumulent. Pour l'argent, utilisez des décimaux à virgule fixe :
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 placesUne réserve importante : toutes les devises n'ont pas deux sous-unités. Le JPY et le KRW ont zéro décimale ; certaines devises comme le BHD et le KWD en ont trois. Arrondissez au bon nombre de décimales selon la devise (en vous appuyant sur la table des sous-unités de l'ISO 4217) plutôt que de fixer 2 en dur. Notre convertisseur de devises et la documentation de l'API reflètent tous deux ces règles propres à chaque devise.
Mettre en cache pour rester dans l'offre gratuite
Les taux de change ne bougent pas de manière significative d'une seconde à l'autre. Pour la plupart des cas d'usage d'affichage, de paiement et de reporting, une actualisation par heure suffit largement — et la mise en cache vous maintient confortablement dans une offre gratuite, quel que soit votre trafic. Ajoutez un simple cache TTL en mémoire protégé par un 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)
}
}Pour que cela compile, vous voudrez #[derive(Clone)] sur RatesResponse. Avec un TTL d'une heure, une seule devise de base vous coûte au maximum 24 requêtes par jour, quel que soit le nombre de conversions effectuées par vos utilisateurs — une erreur d'arrondi face à une offre gratuite de 1 000 requêtes. Si vous la dépassez, les formules tarifaires évoluent de façon linéaire. Pour des schémas comme stale-while-revalidate et les caches partagés entre instances, consultez notre guide de cache et de gestion des erreurs.
Récupérer les taux historiques
Les factures antidatées, les rapports et les graphiques nécessitent des données historiques. Finexly expose un endpoint historique que vous pouvez appeler depuis le même client en ajoutant une méthode qui interroge /v1/historical :
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)
}
}La forme de la réponse est identique à celle du endpoint des derniers taux, si bien que tout votre code d'analyse et de conversion existant fonctionne tel quel. Consultez la documentation de l'API pour la liste complète des paramètres et le endpoint /v1/timeseries pour des plages de dates.
Pièges courants à éviter
- Créer un
Clientpar requête. Construisez-en un, clonez-le (c'est bon marché) et réutilisez-le pour garder le pool de connexions au chaud. - Utiliser
f64pour les soldes stockés. Acceptable pour l'affichage, inadapté pour la comptabilité — utilisezrust_decimalpour tout ce qui doit s'équilibrer. - Coder deux décimales en dur. Le JPY et le KRW en ont zéro ; quelques devises en ont trois. Arrondissez par devise en utilisant les sous-unités de l'ISO 4217.
- Sauter la vérification du statut. Désérialiser un corps de
403ou429comme des données de taux produit des bugs déroutants.error_for_status()gère cela en une ligne. - Interroger à chaque conversion. Mettez en cache avec un TTL et appliquez un debounce à la saisie de l'utilisateur pour ne pas épuiser votre quota.
- Committer votre clé d'API. Lisez-la depuis l'environnement ou un gestionnaire de secrets, jamais depuis un littéral de chaîne dans
main.rs.
Si vous comparez encore les fournisseurs, notre comparatif des API de devises détaille l'exactitude, la couverture des devises et les limites de l'offre gratuite, et le guide des API de change gratuites approfondit les options sans frais.
Foire aux questions
Quel est le meilleur client HTTP pour appeler une API de devises en Rust ?
reqwest est le choix standard pour le Rust asynchrone. Il est bâti sur hyper et tokio, gère le pooling des connexions, le TLS et le JSON d'emblée, et se marie naturellement avec serde pour des réponses typées. Activez la fonctionnalité json et réutilisez une seule instance de Client dans toute votre application.Comment analyser une réponse JSON de taux de change en Rust ?
Définissez une struct qui reflète la réponse et dérivezserde::Deserialize dessus. Modélisez le champ rates comme un HashMap<String, f64> puisqu'il s'agit d'une map plate d'un code de devise vers un taux, puis appelez .json::<RatesResponse>() sur la réponse et reqwest la désérialise pour vous.Dois-je utiliser f64 ou Decimal pour la conversion de devises en Rust ?
Utilisezf64 pour des conversions d'affichage rapides, mais utilisez rust_decimal::Decimal pour tout ce que vous stockez ou rapprochez — soldes, factures, comptabilité. La virgule flottante binaire ne peut pas représenter exactement les fractions décimales, si bien que les erreurs d'arrondi s'accumulent. rust_decimal vous offre une précision à virgule fixe.À quelle fréquence dois-je récupérer les taux de change dans un service Rust ?
Pour la plupart des cas d'usage de paiement, d'affichage et de reporting, une fois par heure suffit largement — les taux ne bougent pas assez en une heure pour que cela compte. Mettez la réponse en cache avec un TTL d'une heure, ce qui vous maintient à environ 24 appels par devise de base et par jour, et confortablement dans une offre gratuite.Puis-je obtenir des taux de change historiques en Rust ?
Oui. Ajoutez une méthode qui appelle le endpoint/v1/historical?date=YYYY-MM-DD sur le même client. La forme du JSON correspond à celle du endpoint des derniers taux, si bien que vos structs et votre logique de conversion existantes fonctionnent sans modification. C'est utile pour les factures antidatées, les rapports et les graphiques.Commencez à construire
Prêt à intégrer des taux de change en temps réel dans votre projet Rust ? Obtenez votre clé gratuite de l'API Finexly — sans carte bancaire. Commencez avec 1 000 requêtes gratuites par mois et passez à une offre supérieure à mesure que vous grandissez. Consultez la documentation de l'API pour la référence complète des endpoints et explorez les formules tarifaires lorsque vous serez prêt à faire évoluer vos fonctionnalités multidevises.
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 →