Voltar ao Blog

Como Construir um Conversor de Moedas com Vue.js e uma API de Taxas de Câmbio em Tempo Real (Guia 2026)

V
Vlado Grigirov
April 28, 2026
Currency API Vue.js Tutorial Exchange Rates JavaScript Finexly Composition API

Se você precisa de um conversor de moedas em Vue que busque taxas de câmbio em tempo real de uma API ao vivo, este tutorial te leva do início ao fim. Você vai construir um componente Vue 3 pronto para produção usando a Composition API, TypeScript e uma API gratuita de taxas de câmbio — com debouncing adequado, cache, tratamento de erros e padrões compatíveis com SSR no Nuxt.

Ao final, você terá um componente reutilizável <CurrencyConverter />, um composable useCurrencyRates que pode ser usado em qualquer projeto Vue 3, e uma compreensão clara dos compromissos que importam ao entregar conversão de moeda para usuários reais.

O que você vai construir

Um conversor de moedas Vue 3 com estes recursos:

  • Taxas de câmbio em tempo real para 170+ moedas via API da Finexly
  • Conversão reativa que atualiza enquanto o usuário digita
  • Botão de troca de moeda (USD → EUR vira EUR → USD com um clique)
  • Chamadas de API com debounce para não sobrecarregar a rede
  • Cache em memória para manter a latência baixa e respeitar a cota do plano gratuito
  • Tratamento de erros, estados de carregamento e tipos TypeScript adequados
  • Compatível com SSR para funcionar no Nuxt 3 sem problemas de hidratação

O componente final tem cerca de 80 linhas. O composable, mais 60. É só isso.

Pré-requisitos

Você deve estar confortável com:

  • Sintaxe Vue 3 (a forma <script setup>)
  • TypeScript básico
  • Chamar uma API REST com fetch

Você também vai precisar de uma chave de API Finexly. Pegue uma no painel — leva uns 30 segundos e o plano gratuito te dá 1.000 requisições por mês sem cartão de crédito. Se nunca usou o serviço, a documentação da API Finexly tem um quickstart de 5 minutos.

Passo 1: Criar um projeto Vue 3 com Vite

Se está começando do zero:

npm create vite@latest finexly-converter -- --template vue-ts
cd finexly-converter
npm install
npm run dev

O Vite te dá Vue 3, TypeScript e hot module reload prontos. Abra src/App.vue e apague o boilerplate — vamos substituí-lo pelo conversor logo.

Se está integrando em um projeto Nuxt 3 existente, pode pular este passo. O composable abaixo funciona idêntico no Nuxt porque usa ref e computed padrão de vue.

Passo 2: Armazenar a chave da API com segurança

Nunca cole sua chave de API Finexly diretamente em um componente. Coloque em .env.local:

VITE_FINEXLY_API_KEY=sua_chave_aqui

O Vite expõe qualquer variável com prefixo VITE_ ao cliente. Para Nuxt, use NUXT_PUBLIC_FINEXLY_API_KEY e leia de useRuntimeConfig().public.finexlyApiKey.

Se está preocupado em expor a chave no bundle do cliente, faça proxy da requisição por uma rota backend ou função serverless. Mostraremos esse padrão no Passo 7.

Passo 3: Construir o composable useCurrencyRates

O composable é o coração do conversor. Ele cuida do fetch, do cache e do estado de loading/erro — assim o componente fica apresentacional.

Crie src/composables/useCurrencyRates.ts:

import { ref, type Ref } from 'vue'

const API_KEY = import.meta.env.VITE_FINEXLY_API_KEY
const BASE_URL = 'https://api.finexly.com/v1'

// Cache em nível de módulo: compartilhado entre todos os componentes que chamam esse composable.
// Cada entrada vive 5 minutos — tempo suficiente pra parecer instantâneo, curto o suficiente
// pra continuar preciso durante movimentos voláteis do FX.
const cache = new Map<string, { rates: Record<string, number>; expires: number }>()
const TTL_MS = 5 * 60 * 1000

interface RatesResponse {
  base: string
  date: string
  rates: Record<string, number>
}

export function useCurrencyRates() {
  const rates: Ref<Record<string, number>> = ref({})
  const loading = ref(false)
  const error = ref<string | null>(null)

  async function fetchRates(base: string) {
    const cached = cache.get(base)
    if (cached && cached.expires > Date.now()) {
      rates.value = cached.rates
      return
    }

    loading.value = true
    error.value = null
    try {
      const url = `${BASE_URL}/latest?base=${base}&apikey=${API_KEY}`
      const res = await fetch(url)
      if (!res.ok) throw new Error(`Finexly returned ${res.status}`)
      const data: RatesResponse = await res.json()

      rates.value = data.rates
      cache.set(base, { rates: data.rates, expires: Date.now() + TTL_MS })
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Failed to load rates'
    } finally {
      loading.value = false
    }
  }

  function convert(amount: number, from: string, to: string): number {
    if (from === to) return amount
    const fromRate = rates.value[from]
    const toRate = rates.value[to]
    if (!fromRate || !toRate) return 0
    return (amount / fromRate) * toRate
  }

  return { rates, loading, error, fetchRates, convert }
}

Alguns detalhes que valem destacar:

  • O cache vive em nível de módulo de propósito. Dois componentes que chamam o composable compartilham o mesmo cache, o que significa que trocar de rota não vai refazer fetch da mesma moeda base.
  • rates é tipado como Record<string, number> para você poder alimentar diretamente um <select> depois.
  • A função convert faz matemática de taxa cruzada, então você não precisa refazer fetch toda vez que a moeda "from" muda. Mais sobre isso no Passo 5.

Passo 4: O componente conversor

Crie src/components/CurrencyConverter.vue:

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useCurrencyRates } from '@/composables/useCurrencyRates'

const { rates, loading, error, fetchRates, convert } = useCurrencyRates()

const amount = ref(100)
const from = ref('USD')
const to = ref('EUR')

const converted = computed(() =>
  convert(amount.value, from.value, to.value).toFixed(2)
)

const currencies = computed(() => Object.keys(rates.value).sort())

function swap() {
  ;[from.value, to.value] = [to.value, from.value]
}

let timeout: ReturnType<typeof setTimeout> | null = null
watch(from, (newBase) => {
  if (timeout) clearTimeout(timeout)
  timeout = setTimeout(() => fetchRates(newBase), 300)
})

onMounted(() => fetchRates(from.value))
</script>

<template>
  <div class="converter">
    <h2>Currency Converter</h2>

    <div class="row">
      <input v-model.number="amount" type="number" min="0" />
      <select v-model="from">
        <option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
      </select>
    </div>

    <button @click="swap" aria-label="Swap currencies">⇅</button>

    <div class="row">
      <output>{{ loading ? '...' : converted }}</output>
      <select v-model="to">
        <option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
      </select>
    </div>

    <p v-if="error" class="error">{{ error }}</p>
  </div>
</template>

Coloque <CurrencyConverter /> em App.vue e você terá um conversor funcionando. A lista de moedas se popula assim que a primeira requisição volta. Digitar no campo de quantidade atualiza o resultado de forma reativa sem tocar na rede. Trocar moedas reordena os dropdowns. Mudar a moeda "from" dispara um refetch com debounce.

Passo 5: Conversão por taxa cruzada (para não queimar requisições)

Se você só converte de uma única base, pode pular esta seção. Mas a maioria dos conversores deixa o usuário escolher os dois lados — e uma implementação ingênua refaz fetch a cada mudança no "from".

A matemática de taxa cruzada resolve isso. Se você tem USD como base e taxas de EUR e JPY em termos de USD, a taxa EUR → JPY é simplesmente (1 / EUR_rate) * JPY_rate. É exatamente o que a função convert do Passo 3 faz:

return (amount / fromRate) * toRate

Isso significa que você só precisa fazer fetch uma vez por sessão. Grande vitória para a cota gratuita. O watcher em from vira uma medida defensiva — se um usuário escolher uma moeda exótica que não está na tabela em cache, refazer fetch com essa base garante que a conversão continue funcionando.

Passo 6: Skeletons de carregamento e estados de erro

Spinners são ok, mas um shift de layout enquanto as taxas carregam é incômodo. Renderize opções placeholder no primeiro fetch:

<select v-model="from" :disabled="loading && currencies.length === 0">
  <option v-if="currencies.length === 0">Loading...</option>
  <option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
</select>

No caminho de erro, mostre um botão de retry em vez de uma mensagem sem saída:

<div v-if="error" class="error-box">
  <p>{{ error }}</p>
  <button @click="fetchRates(from)">Retry</button>
</div>

Trate 429 (rate limited) e 5xx separadamente se quiser ser educado. Com as 1.000 requisições gratuitas mensais da Finexly e cache de 5 minutos, você precisaria de um pico real para bater no limite — mas um caminho de retry limpo deixa a UI sólida.

Passo 7: Esconder sua chave de API com um proxy de servidor

Tudo em import.meta.env.VITE_* acaba no bundle do cliente. Para a maioria dos widgets de moeda só-leitura isso é aceitável — o pior caso é alguém raspando taxas com sua chave. Se quiser defesa em profundidade, faça proxy da requisição no servidor.

Com uma rota de servidor Nuxt 3:

// server/api/rates.get.ts
export default defineEventHandler(async (event) => {
  const { base } = getQuery(event)
  const config = useRuntimeConfig()
  return await $fetch(
    `https://api.finexly.com/v1/latest?base=${base}&apikey=${config.finexlyApiKey}`
  )
})

Depois aponte o composable para /api/rates?base=${base} em vez da origem da Finexly. A chave nunca sai do servidor. O mesmo padrão funciona em qualquer meta-framework Vue — Nuxt, Quasar ou um backend Express simples.

Passo 8: Checklist de produção

Antes de lançar:

  • Cacheie agressivamente. Taxas FX não mudam a cada segundo. TTL de 5 minutos no cliente e 1 minuto no servidor é mais que suficiente para quase qualquer caso de exibição. Veja nosso guia de cache e tratamento de erros para padrões testados em produção.
  • Arredonde corretamente. Exibição de moeda arredonda para a unidade menor da moeda (2 casas para USD, 0 para JPY, 3 para KWD). Use Intl.NumberFormat(locale, { style: 'currency', currency: to.value }) em vez de .toFixed(2).
  • Formate com Intl. new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(123.45) te dá "€123.45" e respeita o locale do usuário.
  • Fetches seguros para SSR. No Nuxt, prefira useFetch em vez de um fetch cru dentro de onMounted para que as taxas estejam disponíveis durante o render do servidor e não dispare mismatch de hidratação.
  • Monitore sua cota. Adicione um pequeno logger que avise quando você queimou 80% do orçamento mensal de requisições. Para volumes maiores, os planos de preço começam onde o plano gratuito termina.

Variante Vue 2 (Options API)

Se está em Vue 2, a mesma lógica traduz quase linha a linha:

export default {
  data() {
    return { rates: {}, loading: false, error: null, amount: 100, from: 'USD', to: 'EUR' }
  },
  computed: {
    converted() {
      const fr = this.rates[this.from]
      const tr = this.rates[this.to]
      return fr && tr ? ((this.amount / fr) * tr).toFixed(2) : '0.00'
    },
    currencies() {
      return Object.keys(this.rates).sort()
    },
  },
  mounted() { this.fetchRates(this.from) },
  watch: {
    from(newBase) { this.fetchRates(newBase) },
  },
  methods: {
    async fetchRates(base) {
      this.loading = true
      try {
        const res = await fetch(
          `https://api.finexly.com/v1/latest?base=${base}&apikey=${process.env.VUE_APP_FINEXLY_KEY}`
        )
        const data = await res.json()
        this.rates = data.rates
      } catch (e) {
        this.error = e.message
      } finally {
        this.loading = false
      }
    },
  },
}

A Composition API é mais limpa para compartilhar lógica entre componentes, mas a Options API funciona bem para um único widget conversor. Se seu time prefere uma abordagem mais funcional, o guia de integração com JavaScript cobre padrões agnósticos a framework.

Pegadinhas comuns (e como evitá-las)

Mismatch de hidratação no Nuxt. Chamar fetch em onMounted funciona no cliente mas quebra a consistência do SSR. Use useFetch ou useAsyncData.

Taxas obsoletas após sessão longa. O TTL de 5 minutos significa que uma aba aberta o dia todo mostra taxas da última hora. Atualize com visibilitychange:

document.addEventListener('visibilitychange', () => {
  if (!document.hidden) fetchRates(from.value)
})

Drift de ponto flutuante. Números em JavaScript são doubles. 0.1 + 0.2 !== 0.3. Para valores monetários, multiplique para centavos inteiros (amount * 100), faça a conta, depois divida. Ou use uma biblioteca como dinero.js para qualquer coisa de checkout.

Erros CORS em desenvolvimento. Algumas APIs de moeda não permitem chamadas direto do navegador. A Finexly permite origens de navegador para uso client-side; o proxy do Passo 7 resolve as outras.

Por que Finexly para projetos Vue

Algumas coisas importam ao escolher uma API de taxas de câmbio para um app frontend: tempo de resposta, precisão, generosidade do plano gratuito e JSON limpo. A Finexly mira nas quatro — latência p95 sub-50ms, taxas mid-market atualizadas a cada 60 segundos, 170+ moedas e um formato JSON que cai direto numa ref do Vue sem ajustes.

Se quer ver como ela se compara a alternativas, nossa comparação de APIs de moeda detalha os trade-offs. Ou use a página de comparação lado a lado.

Perguntas frequentes

Posso usar este tutorial com Vue 2?

Sim. O padrão composable é só Vue 3, mas a lógica subjacente — buscar taxas, guardar em data, computar a conversão — funciona igual no Options API. O exemplo Vue 2 acima é uma substituição direta.

A API Finexly é gratuita para projetos Vue?

O plano gratuito te dá 1.000 requisições por mês, mais que suficiente para um projeto pessoal, peça de portfólio ou SaaS pequeno. Com cache de 5 minutos, isso suporta cerca de 200 usuários ativos diários. Veja planos de preço para volumes maiores.

Como evito expor minha chave de API no bundle do cliente?

Faça proxy da requisição por uma rota de servidor como mostra o Passo 7. Os prefixos VITE_* e NUXT_PUBLIC_* deixam variáveis visíveis ao cliente. Qualquer coisa sensível deve viver atrás de uma função de servidor.

Quão precisas são as taxas?

A Finexly agrega taxas mid-market de múltiplos provedores de liquidez Tier-1 e atualiza a cada 60 segundos. É preciso o suficiente para exibição e a maioria das aplicações de pricing. Para execução de trades, você quer um feed em streaming — veja nosso guia REST vs WebSocket.

Posso converter valores históricos?

Sim — o endpoint /historical da Finexly aceita um parâmetro de data e retorna taxas de qualquer dia útil. O padrão é idêntico ao endpoint /latest acima; só troque a URL. O guia da API de taxas históricas cobre em detalhes.

Encerrando

Você agora tem um conversor de moeda Vue 3 que lida com preocupações do mundo real — cache, debouncing, estados de erro, SSR e segurança da chave da API. O padrão composable significa que você pode jogar o mesmo useCurrencyRates em um widget de navbar, página de checkout ou gráfico de dashboard sem reescrever a lógica de fetch.

Pronto para experimentar no seu próprio projeto? Pegue sua chave gratuita da API Finexly — sem cartão de crédito. Comece com 1.000 requisições gratuitas por mês e faça upgrade só quando seu tráfego crescer além do plano gratuito.

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