Voltar ao Blog

API de taxas de câmbio de moedas em Rust — Tutorial completo de integração

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

Se você está construindo infraestrutura fintech, um motor de precificação, uma ferramenta de linha de comando ou um backend de alto desempenho, mais cedo ou mais tarde vai precisar de dados cambiais em tempo real. Conectar uma API de taxas de câmbio de moedas em Rust é uma das tarefas de rede mais limpas que a linguagem oferece: com reqwest, tokio e serde você ganha um cliente HTTP assíncrono, desserialização automática de JSON e garantias em tempo de compilação de que o seu código de tratamento de taxas está correto antes mesmo de ser executado. Este tutorial percorre todo o caminho — desde a sua primeira requisição GET até um conversor de moedas com cara de produção, com um cliente reutilizável, modelos tipados, tratamento de erros personalizado, aritmética monetária segura com decimais e um cache em memória que mantém você dentro do plano gratuito.

Ao final, você terá um módulo Rust pequeno e reutilizável construído sobre a documentação da API da Finexly que converte entre qualquer uma de mais de 170 moedas com taxas em tempo real. Se você já leu o nosso tutorial da API de moedas em Go, este é o equivalente em Rust com a mesma arquitetura — um cliente tipado, um cache e uma propagação de erros limpa.

Por que Rust combina tão bem com dados cambiais

Os dados cambiais são limitados por E/S, sensíveis à latência e — em qualquer sistema que lide com dinheiro — críticos quanto à exatidão. Rust é excepcionalmente bem adaptado às três coisas:

  • Assíncrono por padrão. O reqwest é construído sobre o hyper e o tokio, então buscas concorrentes de taxas são baratas e não bloqueantes. Você pode solicitar dezenas de pares de moedas em paralelo sem criar threads do sistema operacional.
  • O sistema de tipos captura erros cedo. Uma struct derivada com serde significa que uma resposta malformada ou um campo ausente é um erro em tempo de compilação ou de desserialização, não um misterioso null três camadas abaixo em produção.
  • Sem coletor de lixo, latência previsível. Para um serviço que cota taxas cambiais sob carga, a ausência de pausas do GC faz diferença.
  • Dinheiro seguro com decimais. Com o crate rust_decimal você obtém aritmética de ponto fixo, então nunca envia um bug de arredondamento que transforme € 100,00 em € 99,999999.

O custo é uma configuração um pouco mais trabalhosa do que a de uma linguagem de script, mas o resultado é um cliente de moedas que você pode incorporar a um serviço e no qual pode confiar.

O que você vai construir

Um FinexlyClient reutilizável que:

  1. Busca as últimas taxas para uma moeda base a partir de uma API de taxas de câmbio de moedas.
  2. Desserializa o JSON em structs Rust tipadas com serde.
  3. Converte entre quaisquer duas moedas, mesmo quando nenhuma delas é a base (taxas cruzadas).
  4. Trata erros — falhas de rede, respostas diferentes de 200, limites de taxa — com um tipo de erro personalizado.
  5. Faz cache das respostas em memória com um TTL para que você permaneça bem dentro de um plano gratuito.

Pré-requisitos e configuração do projeto

Você precisa de uma toolchain estável e recente do Rust (instale via rustup) e de uma chave gratuita da API da Finexly. Você pode cadastrar-se gratuitamente em menos de um minuto — sem cartão de crédito, 1.000 requisições por mês no plano gratuito.

Crie um novo projeto e adicione as dependências:

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

As dependências do seu Cargo.toml devem agora ficar assim:

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

Uma observação rápida sobre os crates: reqwest é o cliente HTTP assíncrono de fato para Rust; a feature json traz o serde_json e habilita o auxiliar .json(). serde com derive fornece o #[derive(Deserialize)]. thiserror facilita enums de erro personalizados e ergonômicos. rust_decimal fornece decimais de ponto fixo para dinheiro.

Guarde a sua chave em uma variável de ambiente para que ela nunca vá parar no controle de versão:

export FINEXLY_API_KEY="your_api_key_here"

O formato da resposta da API da Finexly

Antes de escrever qualquer código, observe o que o endpoint retorna. Uma requisição ao endpoint de últimas taxas:

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

retorna um objeto JSON pequeno e previsível:

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

Duas coisas moldam o código abaixo. Primeiro, rates é um mapa plano de código de moeda ISO 4217 para um número de ponto flutuante — o que mapeia de forma limpa para um HashMap<String, f64> do Rust. Segundo, base informa em relação a que essas taxas estão. Para converter USD → EUR você multiplica por rates["EUR"]. Para converter entre duas moedas que não são a base, você passa pela base: divide por uma e multiplica pela outra.

Fazendo a sua primeira requisição com reqwest

Comece com a requisição assíncrona mais simples possível. Substitua o src/main.rs por:

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

Execute com cargo run e você verá o JSON bruto impresso. A macro #[tokio::main] configura o runtime assíncrono, o reqwest::get realiza a requisição, e cada .await? suspende a tarefa até que a rede responda, propagando qualquer erro. Isso funciona, mas o reqwest::get cria um cliente totalmente novo a cada chamada — tudo bem para algo pontual, mas errado para uma aplicação real. Vamos corrigir isso em breve.

Modelando o JSON com Serde

Imprimir uma string não é útil; você quer dados tipados. Defina structs que espelhem a resposta e deixe o serde fazer a análise:

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

Agora troque .text() por .json(), que desserializa o corpo diretamente para a sua struct:

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

O método json() lê o corpo da resposta e executa o serde_json por baixo dos panos. Se um campo estiver ausente ou tiver o tipo errado, você recebe um erro de desserialização claro em vez de um null silencioso. Como rates é um HashMap, você pode consultar qualquer moeda retornada pelo seu código ISO 4217.

Construindo um cliente reutilizável

O erro mais comum com reqwest é criar um novo Client para cada requisição. Cada cliente possui o seu próprio pool de conexões, então recriá-lo descarta as conexões keep-alive e os handshakes TLS. Crie um único Client, clone-o de forma barata (internamente ele é um Arc) e reutilize-o em todos os lugares. Envolva-o em uma pequena struct que oculte a URL e a chave:

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 a pena destacar algumas coisas. O construtor .query(&[...]) cuida da codificação da URL para você, então você nunca concatena manualmente strings de consulta. O .timeout(...) no builder protege contra uma conexão travada paralisar o seu serviço. E o error_for_status() transforma qualquer resposta HTTP diferente de 2xx em um Err — o que nos leva ao tratamento de erros.

Tratamento robusto de erros

O código de produção precisa responder: o que acontece quando a rede está fora do ar, a chave é inválida (403) ou você atinge o limite de taxa (429)? Modele esses casos com um enum de erro personalizado usando o 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),
}

A linha #[from] reqwest::Error significa que qualquer falha do reqwest — DNS, TLS, timeout ou um status diferente de 2xx capturado pelo error_for_status() — é automaticamente convertida em FinexlyError::Http por meio do operador ?. É por isso que cada .await? em latest() simplesmente funciona.

Você também pode inspecionar o código de status explicitamente quando quiser um comportamento sob medida — por exemplo, aplicar backoff diante de um 429:

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

Para um tratamento mais aprofundado de retentativas, backoff e estratégia de cache, veja o nosso guia de melhores práticas de cache e tratamento de erros para APIs de moedas.

Convertendo entre quaisquer duas moedas

Multiplicar por uma única taxa só funciona quando a sua moeda de origem é a base. Aplicações reais precisam de pares arbitrários — EUR → JPY, GBP → CAD — onde nenhum dos lados é a base. O truque é rotear pela moeda base: divida o valor pela taxa de origem para obter o equivalente na base e, em seguida, multiplique pela taxa de destino.

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

Aqui simplesmente solicitamos as taxas com from como base, então a taxa retornada já é a conversão direta from → to. Se você fizer cache de uma moeda base (digamos USD) e precisar de taxas cruzadas, calcule-as no lado do cliente: amount / rates[from] * rates[to]. Experimente de ponta a ponta:

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

Usando rust_decimal para dinheiro

O f64 é adequado para conversões de exibição, mas é o tipo errado para armazenar saldos, faturar ou qualquer coisa que precise fechar até o centavo. O ponto flutuante binário não consegue representar 0.1 exatamente, e esses erros se acumulam. Para dinheiro, use decimais de ponto fixo:

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

Uma ressalva importante: nem toda moeda tem duas unidades menores. O JPY e o KRW têm zero casas decimais; algumas moedas como o BHD e o KWD têm três. Arredonde para o número correto de casas decimais por moeda (orientado pela tabela de unidades menores da ISO 4217) em vez de fixar 2 de forma rígida. O nosso conversor de moedas e a documentação da API refletem essas regras por moeda.

Fazendo cache para permanecer dentro do plano gratuito

As taxas de câmbio não se movem de forma significativa de segundo a segundo. Para a maioria dos casos de uso de exibição, checkout e relatórios, atualizar uma vez por hora é mais do que suficiente — e o cache mantém você confortavelmente dentro de um plano gratuito, não importa quanto tráfego você receba. Adicione um cache TTL simples em memória protegido por um 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)
    }
}

Para que isso compile, você vai querer #[derive(Clone)] em RatesResponse. Com um TTL de uma hora, uma única moeda base custa no máximo 24 requisições por dia, independentemente de quantas conversões os seus usuários realizem — um erro de arredondamento diante de um plano gratuito de 1.000 requisições. Se você ultrapassá-lo, os planos de preços escalam de forma linear. Para padrões como stale-while-revalidate e caches compartilhados entre instâncias, veja o nosso guia de cache e tratamento de erros.

Buscando taxas históricas

Faturas retroativas, relatórios e gráficos precisam de dados históricos. A Finexly expõe um endpoint histórico que você pode chamar a partir do mesmo cliente adicionando um método que acessa /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)
    }
}

O formato da resposta é idêntico ao do endpoint de últimas taxas, então todo o seu código de análise e conversão existente simplesmente funciona. Consulte a documentação da API para a lista completa de parâmetros e o endpoint /v1/timeseries para intervalos de datas.

Armadilhas comuns a evitar

  • Criar um Client por requisição. Construa um, clone-o (é barato) e reutilize-o para manter o pool de conexões aquecido.
  • Usar f64 para saldos armazenados. Adequado para exibição, errado para contabilidade — use rust_decimal para qualquer coisa que precise fechar as contas.
  • Fixar duas casas decimais de forma rígida. O JPY e o KRW têm zero; algumas poucas moedas têm três. Arredonde por moeda usando as unidades menores da ISO 4217.
  • Pular a verificação de status. Desserializar um corpo de 403 ou 429 como dados de taxas produz bugs confusos. O error_for_status() cuida disso em uma linha.
  • Buscar a cada conversão. Faça cache com um TTL e aplique debounce à entrada do usuário para não consumir a sua cota.
  • Enviar a sua chave de API ao repositório. Leia-a do ambiente ou de um gerenciador de segredos, nunca de um literal de string em main.rs.

Se você ainda está comparando provedores, a nossa comparação de APIs de moedas detalha a exatidão, a cobertura de moedas e os limites do plano gratuito, e o guia de APIs de câmbio gratuitas aborda as opções sem custo com mais profundidade.

Perguntas frequentes

Qual é o melhor cliente HTTP para chamar uma API de moedas em Rust?

O reqwest é a escolha padrão para Rust assíncrono. Ele é construído sobre o hyper e o tokio, cuida do pooling de conexões, TLS e JSON de fábrica, e combina naturalmente com o serde para respostas tipadas. Habilite a feature json e reutilize uma única instância de Client em toda a sua aplicação.

Como faço para analisar uma resposta JSON de taxas de câmbio em Rust?

Defina uma struct que espelhe a resposta e derive serde::Deserialize nela. Modele o campo rates como um HashMap<String, f64>, já que é um mapa plano de código de moeda para taxa, depois chame .json::<RatesResponse>() na resposta e o reqwest a desserializa para você.

Devo usar f64 ou Decimal para conversão de moedas em Rust?

Use f64 para conversões rápidas de exibição, mas use rust_decimal::Decimal para qualquer coisa que você armazene ou concilie — saldos, faturas, contabilidade. O ponto flutuante binário não consegue representar frações decimais exatamente, então os erros de arredondamento se acumulam. O rust_decimal oferece precisão de ponto fixo.

Com que frequência devo buscar as taxas de câmbio em um serviço Rust?

Para a maioria dos casos de uso de checkout, exibição e relatórios, uma vez por hora é mais do que suficiente — as taxas não se movem o bastante em uma hora para importar. Faça cache da resposta com um TTL de uma hora, o que mantém você em cerca de 24 chamadas por moeda base por dia e confortavelmente dentro de um plano gratuito.

Posso obter taxas de câmbio históricas em Rust?

Sim. Adicione um método que chame o endpoint /v1/historical?date=YYYY-MM-DD no mesmo cliente. O formato do JSON corresponde ao do endpoint de últimas taxas, então as suas structs e a sua lógica de conversão existentes funcionam sem alterações. Isso é útil para faturas retroativas, relatórios e gráficos.

Comece a construir

Pronto para integrar taxas de câmbio em tempo real ao seu projeto Rust? Obtenha a sua chave gratuita da API da Finexly — sem cartão de crédito. Comece com 1.000 requisições gratuitas por mês e faça upgrade conforme você cresce. Consulte a documentação da API para a referência completa de endpoints e explore os planos de preços quando estiver pronto para escalar os seus recursos multimoeda.

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 →

Compartilhar este artigo