Retour au blog

Comment Construire un Convertisseur de Devises avec Vue.js et une API de Taux de Change en Temps Réel (Guide 2026)

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

Si vous avez besoin d'un convertisseur de devises Vue qui récupère les taux de change en temps réel depuis une API live, ce tutoriel vous accompagne de bout en bout. Vous allez construire un composant Vue 3 prêt pour la production avec la Composition API, TypeScript et une API gratuite de taux de change — avec debouncing, mise en cache, gestion d'erreur et patterns SSR-safe pour Nuxt.

À la fin, vous aurez un composant <CurrencyConverter /> réutilisable, un composable useCurrencyRates à intégrer dans n'importe quel projet Vue 3, et une compréhension claire des compromis qui comptent quand on livre la conversion de devises à de vrais utilisateurs.

Ce que vous allez construire

Un convertisseur de devises Vue 3 avec ces fonctionnalités :

  • Taux de change live pour 170+ devises via l'API Finexly
  • Conversion réactive qui se met à jour pendant la saisie
  • Bouton d'inversion (USD → EUR devient EUR → USD en un clic)
  • Appels API debouncés pour ne pas saturer le réseau
  • Cache en mémoire pour garder une faible latence et respecter les quotas du plan gratuit
  • Gestion d'erreur, états de chargement et types TypeScript propres
  • Compatible SSR pour fonctionner dans Nuxt 3 sans mismatch d'hydratation

Le composant final fait environ 80 lignes. Le composable, 60 de plus. C'est tout.

Prérequis

Vous devriez être à l'aise avec :

  • La syntaxe Vue 3 (la forme <script setup>)
  • TypeScript de base
  • Appeler une API REST avec fetch

Il vous faudra aussi une clé API Finexly. Récupérez-en une depuis le tableau de bord — ça prend 30 secondes et le plan gratuit donne 1 000 requêtes par mois sans carte bancaire. Si vous n'avez jamais utilisé le service, la documentation de l'API Finexly propose un quickstart de 5 minutes.

Étape 1 : Créer un projet Vue 3 avec Vite

Si vous partez de zéro :

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

Vite vous fournit Vue 3, TypeScript et le hot module reload prêts à l'emploi. Ouvrez src/App.vue et nettoyez le boilerplate — on va le remplacer par le convertisseur sous peu.

Si vous intégrez à un projet Nuxt 3 existant, vous pouvez sauter cette étape. Le composable ci-dessous fonctionne à l'identique dans Nuxt car il utilise les ref et computed standard de vue.

Étape 2 : Stocker la clé API en sécurité

Ne collez jamais votre clé API Finexly directement dans un composant. Mettez-la dans .env.local :

VITE_FINEXLY_API_KEY=votre_cle_ici

Vite expose toute variable préfixée VITE_ au client. Pour Nuxt, utilisez NUXT_PUBLIC_FINEXLY_API_KEY et lisez-la depuis useRuntimeConfig().public.finexlyApiKey.

Si l'exposition de la clé dans le bundle client vous inquiète, faites passer la requête par une route backend ou une fonction serverless. On montre ce pattern à l'étape 7.

Étape 3 : Construire le composable useCurrencyRates

Le composable est le cœur du convertisseur. Il gère le fetch, le cache et l'état de chargement/erreur — ce qui laisse le composant purement présentationnel.

Créez 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 au niveau du module : partagé entre tous les composants qui appellent ce composable.
// Chaque entrée vit 5 minutes — assez pour paraître instantané, assez court pour rester
// précis pendant les mouvements volatils du 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 }
}

Quelques détails à noter :

  • Le cache vit au niveau du module à dessein. Deux composants qui appellent le composable partagent le même cache, ce qui veut dire que changer de route ne refetchera pas la même devise de base.
  • rates est typé Record<string, number> pour pouvoir alimenter directement un <select> plus tard.
  • La fonction convert fait du calcul de taux croisé, donc vous n'avez pas à refetch à chaque changement de la devise "from". Plus de détails à l'étape 5.

Étape 4 : Le composant convertisseur

Créez 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>

Placez <CurrencyConverter /> dans App.vue et vous avez un convertisseur fonctionnel. La liste des devises se remplit dès que le premier fetch revient. Saisir dans le champ montant met à jour le résultat de manière réactive sans toucher au réseau. L'inversion réordonne les listes déroulantes. Changer la devise "from" déclenche un refetch debouncé.

Étape 5 : Conversion par taux croisé (pour ne pas brûler de requêtes)

Si vous ne convertissez que depuis une seule base, vous pouvez sauter cette section. Mais la plupart des convertisseurs laissent l'utilisateur choisir les deux côtés — et une implémentation naïve refetch à chaque changement du "from".

Le calcul de taux croisé règle ça. Si vous avez USD comme base et les taux EUR et JPY en USD, le taux EUR → JPY vaut simplement (1 / EUR_rate) * JPY_rate. C'est exactement ce que fait la fonction convert de l'étape 3 :

return (amount / fromRate) * toRate

Cela signifie que vous n'avez besoin de fetcher qu'une fois par session. Gros gain pour les quotas du plan gratuit. Le watcher sur from devient une mesure défensive — si un utilisateur choisit une devise exotique absente de la table en cache, refetcher avec cette base garantit que la conversion fonctionne toujours.

Étape 6 : Skeletons de chargement et états d'erreur

Les spinners sont OK, mais un saut de mise en page pendant le chargement des taux est gênant. Affichez des options placeholder lors du premier 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>

Pour le chemin d'erreur, montrez un bouton réessayer plutôt qu'un message sans issue :

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

Traitez 429 (rate limited) et 5xx séparément si vous voulez être courtois. Avec les 1 000 requêtes mensuelles gratuites de Finexly et le cache de 5 minutes, il faudrait un vrai pic pour atteindre la limite — mais un chemin de retry propre rend l'UI solide.

Étape 7 : Cacher votre clé API derrière un proxy serveur

Tout ce qui est dans import.meta.env.VITE_* se retrouve dans le bundle client. Pour la plupart des widgets de devises en lecture seule c'est acceptable — le pire scénario est que quelqu'un scrape vos taux avec votre clé. Si vous voulez de la défense en profondeur, faites passer la requête par le serveur.

Avec une route serveur 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}`
  )
})

Pointez ensuite le composable vers /api/rates?base=${base} au lieu de l'origine Finexly. La clé ne quitte jamais le serveur. Le même pattern fonctionne dans n'importe quel meta-framework Vue — Nuxt, Quasar ou un backend Express simple.

Étape 8 : Checklist de mise en production

Avant de livrer :

  • Cachez agressivement. Les taux FX ne changent pas chaque seconde. TTL de 5 minutes côté client et 1 minute côté serveur suffit largement à presque tous les cas d'affichage. Voir notre guide cache et gestion d'erreur pour les patterns testés en production.
  • Arrondissez correctement. L'affichage de devise s'arrondit à l'unité mineure de la devise (2 décimales pour USD, 0 pour JPY, 3 pour KWD). Utilisez Intl.NumberFormat(locale, { style: 'currency', currency: to.value }) plutôt que .toFixed(2).
  • Formatez avec Intl. new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(123.45) donne « €123.45 » et respecte la locale de l'utilisateur.
  • Fetches SSR-safe. Dans Nuxt, préférez useFetch à un fetch brut dans onMounted pour que les taux soient dispos pendant le rendu serveur sans déclencher de mismatch d'hydratation.
  • Surveillez votre quota. Ajoutez un petit logger qui prévient quand vous avez consommé 80 % du budget mensuel. Pour des volumes plus gros, les forfaits commencent là où le plan gratuit s'arrête.

Variante Vue 2 (Options API)

Si vous êtes en Vue 2, la même logique se traduit presque ligne par ligne :

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

La Composition API est plus propre pour partager de la logique entre composants, mais l'Options API marche bien pour un widget convertisseur unique. Si votre équipe préfère une approche plus fonctionnelle, le guide d'intégration JavaScript couvre des patterns agnostiques au framework.

Pièges courants (et comment les éviter)

Mismatch d'hydratation dans Nuxt. Appeler fetch dans onMounted marche côté client mais casse la cohérence SSR. Utilisez useFetch ou useAsyncData à la place.

Taux périmés après une longue session. Le TTL de 5 minutes ci-dessus signifie qu'un onglet ouvert toute la journée affiche les taux d'il y a une heure. Rafraîchissez via visibilitychange :

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

Dérive en virgule flottante. Les nombres JavaScript sont des doubles. 0.1 + 0.2 !== 0.3. Pour les montants monétaires, multipliez en centimes entiers (amount * 100), faites le calcul, puis divisez. Ou utilisez une bibliothèque comme dinero.js pour tout ce qui touche au paiement.

Erreurs CORS en développement. Certaines APIs de devises interdisent les appels directs depuis le navigateur. Finexly autorise les origines navigateur pour l'usage côté client ; le proxy de l'étape 7 règle les autres cas.

Pourquoi Finexly pour les projets Vue

Quelques critères comptent quand on choisit une API de taux de change pour une app frontend : temps de réponse, précision, générosité du plan gratuit et JSON propre. Finexly vise les quatre — latence p95 sous 50 ms, taux mid-market mis à jour toutes les 60 secondes, 170+ devises et une forme JSON qui tombe directement dans une ref Vue sans massage.

Pour voir comment ça se compare aux alternatives, notre comparaison d'APIs de devises détaille les compromis. Ou utilisez la page de comparaison côte à côte.

Foire aux questions

Puis-je utiliser ce tutoriel avec Vue 2 ?

Oui. Le pattern composable est exclusif à Vue 3, mais la logique sous-jacente — fetcher les taux, les stocker dans data, calculer la conversion — fonctionne à l'identique en Options API. L'exemple Vue 2 ci-dessus est un remplacement direct.

L'API Finexly est-elle gratuite pour les projets Vue ?

Le plan gratuit donne 1 000 requêtes par mois, ce qui suffit largement pour un projet perso, une pièce de portfolio ou un petit SaaS. Avec un cache de 5 minutes, ça supporte environ 200 utilisateurs actifs par jour. Voir les forfaits pour des volumes plus gros.

Comment éviter d'exposer ma clé API dans le bundle client ?

Faites passer la requête par une route serveur comme à l'étape 7. Les préfixes VITE_* et NUXT_PUBLIC_* rendent les variables visibles au client. Tout ce qui est sensible doit vivre derrière une fonction serveur.

À quel point les taux sont-ils précis ?

Finexly agrège des taux mid-market depuis plusieurs fournisseurs de liquidité Tier-1 et rafraîchit toutes les 60 secondes. C'est assez précis pour l'affichage et la plupart des applications de pricing. Pour de l'exécution de trade, vous voudrez un flux streaming — voir notre guide REST vs WebSocket.

Puis-je convertir des montants historiques ?

Oui — l'endpoint /historical de Finexly accepte un paramètre date et retourne les taux de n'importe quel jour ouvré. Le pattern est identique à l'endpoint /latest ci-dessus ; il suffit de changer l'URL. Le guide de l'API de taux historiques le couvre en détail.

Conclusion

Vous avez maintenant un convertisseur de devises Vue 3 qui gère les vrais soucis de prod — cache, debouncing, états d'erreur, SSR et sécurité de la clé API. Le pattern composable veut dire que vous pouvez glisser le même useCurrencyRates dans un widget de barre de navigation, une page de paiement ou un graphique de tableau de bord sans réécrire la logique de fetch.

Prêt à l'essayer sur votre propre projet ? Récupérez votre clé API Finexly gratuite — sans carte bancaire. Commencez avec 1 000 requêtes gratuites par mois et upgradez quand votre trafic dépasse le plan gratuit.

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 →

Partager cet article