Torna al Blog

API dei tassi di cambio in Rust — Tutorial completo di integrazione

V
Vlado Grigirov
August 04, 2026
Rust Currency API Exchange Rates Tutorial reqwest Tokio Fintech

Se stai costruendo un'infrastruttura fintech, un motore di pricing, uno strumento CLI o un backend ad alto throughput, prima o poi avrai bisogno di dati sui cambi valutari in tempo reale. Collegare un'API dei tassi di cambio in Rust è una delle attività di rete più pulite che il linguaggio abbia da offrire: con reqwest, tokio e serde ottieni un client HTTP asincrono, la deserializzazione automatica del JSON e garanzie a tempo di compilazione che il tuo codice per la gestione dei tassi sia corretto ancora prima di essere eseguito. Questo tutorial percorre l'intero cammino — dalla tua prima richiesta GET fino a un convertitore di valute pronto per la produzione, con un client riutilizzabile, modelli tipizzati, gestione degli errori personalizzata, aritmetica monetaria sicura con i decimali e una cache in memoria che ti mantiene all'interno del piano gratuito.

Alla fine avrai un piccolo modulo Rust riutilizzabile costruito sulla documentazione dell'API Finexly, che converte tra oltre 170 valute con tassi in tempo reale. Se hai già letto il nostro tutorial sull'API delle valute in Go, questa è la controparte in Rust con la stessa architettura — un client tipizzato, una cache e una propagazione degli errori pulita.

Perché Rust si presta bene ai dati valutari

I dati valutari sono I/O-bound, sensibili alla latenza e — in qualsiasi sistema che tocchi il denaro — critici per quanto riguarda la correttezza. Rust è insolitamente adatto a tutti e tre gli aspetti:

  • Asincrono per impostazione predefinita. reqwest è costruito su hyper e tokio, quindi i recuperi di tassi concorrenti sono economici e non bloccanti. Puoi richiedere decine di coppie di valute in parallelo senza creare thread del sistema operativo.
  • Il sistema dei tipi individua gli errori in anticipo. Uno struct derivato da serde significa che una risposta malformata o un campo mancante è un errore a tempo di compilazione o di deserializzazione, non un misterioso null tre livelli sotto in produzione.
  • Nessun garbage collector, latenza prevedibile. Per un servizio che quota tassi FX sotto carico, l'assenza di pause del GC conta.
  • Denaro sicuro con i decimali. Con la crate rust_decimal ottieni l'aritmetica in virgola fissa, così non rilasci mai un bug di arrotondamento che trasforma 100,00 € in 99,999999 €.

Il compromesso è una configurazione leggermente più impegnativa rispetto a un linguaggio di scripting, ma il risultato è un client valutario che puoi inserire in un servizio e di cui fidarti.

Cosa costruirai

Un FinexlyClient riutilizzabile che:

  1. Recupera i tassi più recenti per una valuta di base da un'API dei tassi di cambio.
  2. Deserializza il JSON in struct Rust tipizzati con serde.
  3. Converte tra due valute qualsiasi, anche quando nessuna delle due è la base (tassi incrociati).
  4. Gestisce gli errori — guasti di rete, risposte diverse da 200, limiti di frequenza — con un tipo di errore personalizzato.
  5. Memorizza in cache le risposte in memoria con un TTL così da restare comodamente all'interno di un piano gratuito.

Prerequisiti e configurazione del progetto

Ti serve una toolchain Rust stabile e recente (installala tramite rustup) e una chiave API Finexly gratuita. Puoi registrarti gratuitamente in meno di un minuto — nessuna carta di credito richiesta, 1.000 richieste al mese nel piano gratuito.

Crea un nuovo progetto e aggiungi le dipendenze:

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_macros

Le dipendenze del tuo Cargo.toml dovrebbero ora avere questo aspetto:

[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"

Una breve nota sulle crate: reqwest è di fatto il client HTTP asincrono per Rust; la feature json include serde_json e abilita l'helper .json(). serde con derive ti offre #[derive(Deserialize)]. thiserror rende ergonomiche le enum di errore personalizzate. rust_decimal fornisce decimali in virgola fissa per il denaro.

Conserva la tua chiave in una variabile d'ambiente così che non finisca mai nel controllo di versione:

export FINEXLY_API_KEY="your_api_key_here"

La forma della risposta dell'API Finexly

Prima di scrivere qualsiasi codice, osserva cosa restituisce l'endpoint. Una richiesta all'endpoint dei tassi più recenti:

curl "https://api.finexly.com/v1/latest?base=USD&symbols=EUR,GBP,JPY&apikey=YOUR_KEY"

restituisce un oggetto JSON piccolo e prevedibile:

{
  "success": true,
  "base": "USD",
  "timestamp": 1753660800,
  "rates": {
    "EUR": 0.9213,
    "GBP": 0.7847,
    "JPY": 161.42
  }
}

Due cose plasmano il codice che segue. Primo, rates è una mappa piatta dal codice valutario ISO 4217 a un numero in virgola mobile — che si mappa in modo pulito su una HashMap<String, f64> di Rust. Secondo, base ti dice a cosa sono relativi quei tassi. Per convertire USD → EUR moltiplichi per rates["EUR"]. Per convertire tra due valute non di base passi attraverso la base: dividi via l'una e moltiplichi dentro l'altra.

Effettuare la tua prima richiesta con reqwest

Inizia con la richiesta asincrona più semplice possibile. Sostituisci src/main.rs con:

#[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(())
}

Eseguilo con cargo run e vedrai stampato il JSON grezzo. La macro #[tokio::main] configura il runtime asincrono, reqwest::get esegue la richiesta e ogni .await? sospende il task finché la rete non risponde, propagando nel frattempo qualsiasi errore. Funziona, ma reqwest::get crea un client nuovo di zecca a ogni chiamata — va bene per un caso isolato, sbagliato per un'applicazione reale. Lo sistemeremo a breve.

Modellare il JSON con Serde

Stampare una stringa non è utile; vuoi dati tipizzati. Definisci degli struct che rispecchiano la risposta e lascia che serde faccia il parsing:

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>,
}

Ora sostituisci .text() con .json(), che deserializza il body direttamente nel tuo struct:

let data: RatesResponse = reqwest::get(&url).await?.json().await?;
println!("1 USD = {} EUR", data.rates["EUR"]);

Il metodo json() legge il body della risposta ed esegue serde_json sotto il cofano. Se un campo è mancante o del tipo sbagliato, ottieni un chiaro errore di deserializzazione invece di un silenzioso null. Poiché rates è una HashMap, puoi cercare qualsiasi valuta restituita tramite il suo codice ISO 4217.

Costruire un client riutilizzabile

L'errore più comune in assoluto con reqwest è creare un nuovo Client per ogni richiesta. Ogni client possiede il proprio pool di connessioni, quindi ricrearlo butta via le connessioni keep-alive e gli handshake TLS. Crea un solo Client, clonalo a basso costo (internamente è un Arc) e riutilizzalo ovunque. Avvolgilo in un piccolo struct che nasconde l'URL e la chiave:

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)
    }
}

Vale la pena sottolineare alcune cose. Il builder .query(&[...]) gestisce per te la codifica dell'URL, così non concateni mai manualmente le query string. Il .timeout(...) sul builder protegge dal fatto che una connessione bloccata rallenti il tuo servizio. E error_for_status() trasforma qualsiasi risposta HTTP diversa da 2xx in un Err — il che ci porta alla gestione degli errori.

Gestione robusta degli errori

Il codice di produzione deve rispondere: cosa succede quando la rete è giù, la chiave non è valida (403) o raggiungi il limite di frequenza (429)? Modella questi casi con un'enum di errore personalizzata usando 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 riga #[from] reqwest::Error significa che qualsiasi guasto di reqwest — DNS, TLS, timeout o uno stato diverso da 2xx intercettato da error_for_status() — viene automaticamente convertito in FinexlyError::Http tramite l'operatore ?. Ecco perché ogni .await? in latest() funziona senza problemi.

Puoi anche ispezionare esplicitamente il codice di stato quando vuoi un comportamento su misura — per esempio, applicando un backoff su un 429:

if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
    // sleep and retry, or fall back to a cached rate
}

Per una trattazione più approfondita di retry, backoff e strategia di caching, consulta la nostra guida alle best practice per il caching e la gestione degli errori delle API valutarie.

Convertire tra due valute qualsiasi

Moltiplicare per un singolo tasso funziona solo quando la tua valuta di partenza è la base. Le applicazioni reali hanno bisogno di coppie arbitrarie — EUR → JPY, GBP → CAD — dove nessuno dei due lati è la base. Il trucco è passare attraverso la valuta di base: dividi l'importo per il tasso di partenza per ottenere l'equivalente nella base, poi moltiplica per il tasso di destinazione.

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)
    }
}

Qui richiediamo semplicemente i tassi con from come base, così il tasso restituito è già la conversione diretta from → to. Se metti in cache una valuta di base (diciamo USD) e ti servono tassi incrociati, calcolali invece lato client: amount / rates[from] * rates[to]. Provalo dall'inizio alla fine:

#[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(())
}

Usare rust_decimal per il denaro

f64 va bene per le conversioni di visualizzazione, ma è il tipo sbagliato per memorizzare saldi, emettere fatture o qualsiasi cosa debba quadrare al centesimo. La virgola mobile binaria non può rappresentare 0.1 esattamente, e quegli errori si accumulano. Per il denaro, usa i decimali in virgola fissa:

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 places

Un'avvertenza importante: non tutte le valute hanno due unità minori. JPY e KRW hanno zero cifre decimali; alcune valute come BHD e KWD ne hanno tre. Arrotonda al numero corretto di decimali per ciascuna valuta (guidato dalla tabella delle unità minori ISO 4217) invece di codificare a mano 2. Il nostro convertitore di valute e la documentazione dell'API riflettono entrambi queste regole specifiche per valuta.

Caching per restare all'interno del piano gratuito

I tassi di cambio non si muovono in modo significativo di secondo in secondo. Per la maggior parte dei casi d'uso di visualizzazione, checkout e reporting, aggiornarli una volta all'ora è più che sufficiente — e il caching ti mantiene comodamente all'interno di un piano gratuito, per quanto traffico tu riceva. Aggiungi una semplice cache TTL in memoria protetta da 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)
    }
}

Perché questo compili, ti conviene avere #[derive(Clone)] su RatesResponse. Con un TTL di un'ora, una singola valuta di base ti costa al massimo 24 richieste al giorno, indipendentemente da quante conversioni eseguano i tuoi utenti — un errore di arrotondamento a fronte di un piano gratuito da 1.000 richieste. Se lo superi, i piani tariffari scalano in modo lineare. Per schemi come stale-while-revalidate e cache condivise tra istanze, consulta la nostra guida al caching e alla gestione degli errori.

Recuperare i tassi storici

Fatture retrodatate, reporting e grafici hanno bisogno di dati storici. Finexly espone un endpoint storico che puoi chiamare dallo stesso client aggiungendo un metodo che punta a /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 forma della risposta è identica a quella dell'endpoint dei tassi più recenti, quindi tutto il tuo codice di parsing e conversione esistente funziona senza modifiche. Consulta la documentazione dell'API per l'elenco completo dei parametri e l'endpoint /v1/timeseries per gli intervalli di date.

Errori comuni da evitare

  • Creare un Client per ogni richiesta. Costruiscine uno, clonalo (è economico) e riutilizzalo così da tenere caldo il pool di connessioni.
  • Usare f64 per i saldi memorizzati. Va bene per la visualizzazione, sbagliato per i libri contabili — usa rust_decimal per qualsiasi cosa debba quadrare.
  • Codificare a mano due cifre decimali. JPY e KRW ne hanno zero; alcune valute ne hanno tre. Arrotonda per valuta usando le unità minori ISO 4217.
  • Saltare il controllo dello stato. Deserializzare un body 403 o 429 come dati di tasso produce bug confusi. error_for_status() gestisce questo in una riga.
  • Recuperare a ogni conversione. Metti in cache con un TTL ed esegui il debounce dell'input dell'utente così da non bruciare la tua quota.
  • Committare la tua chiave API. Leggila dall'ambiente o da un secrets manager, mai da un literal stringa in main.rs.

Se stai ancora confrontando i fornitori, il nostro confronto delle API valutarie analizza accuratezza, copertura delle valute e limiti del piano gratuito, e la guida alle API valutarie gratuite approfondisce più a fondo le opzioni a costo zero.

Domande frequenti

Qual è il miglior client HTTP per chiamare un'API valutaria in Rust?

reqwest è la scelta standard per il Rust asincrono. È costruito su hyper e tokio, gestisce il connection pooling, il TLS e il JSON out of the box, e si abbina naturalmente a serde per le risposte tipizzate. Abilita la feature json e riutilizza una singola istanza di Client in tutta la tua applicazione.

Come faccio il parsing di una risposta JSON con i tassi di cambio in Rust?

Definisci uno struct che rispecchia la risposta e derivaci serde::Deserialize. Modella il campo rates come HashMap<String, f64> dato che è una mappa piatta dal codice valutario al tasso, poi chiama .json::<RatesResponse>() sulla risposta e reqwest la deserializza per te.

Dovrei usare f64 o Decimal per la conversione di valuta in Rust?

Usa f64 per le conversioni di visualizzazione rapide, ma usa rust_decimal::Decimal per qualsiasi cosa tu memorizzi o riconcili — saldi, fatture, libri contabili. La virgola mobile binaria non può rappresentare esattamente le frazioni decimali, quindi gli errori di arrotondamento si accumulano. rust_decimal ti offre la precisione in virgola fissa.

Con quale frequenza dovrei recuperare i tassi di cambio in un servizio Rust?

Per la maggior parte dei casi d'uso di checkout, visualizzazione e reporting, una volta all'ora è più che sufficiente — i tassi non si muovono abbastanza entro un'ora da avere importanza. Metti in cache la risposta con un TTL di un'ora, il che ti mantiene a circa 24 chiamate per valuta di base al giorno e comodamente all'interno di un piano gratuito.

Posso ottenere tassi di cambio storici in Rust?

Sì. Aggiungi un metodo che chiama l'endpoint /v1/historical?date=YYYY-MM-DD sullo stesso client. La forma del JSON corrisponde a quella dell'endpoint dei tassi più recenti, quindi i tuoi struct esistenti e la tua logica di conversione funzionano senza modifiche. Questo è utile per fatture retrodatate, reporting e grafici.

Inizia a costruire

Pronto a integrare i tassi di cambio in tempo reale nel tuo progetto Rust? Ottieni la tua chiave API Finexly gratuita — nessuna carta di credito richiesta. Inizia con 1.000 richieste gratuite al mese ed effettua l'upgrade man mano che cresci. Consulta la documentazione dell'API per il riferimento completo degli endpoint ed esplora i piani tariffari quando sei pronto a scalare le tue funzionalità multivaluta.

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 →

Condividi questo articolo