Terug naar Blog

Hoe bouw je een valutaconverter met Vue.js en een live wisselkoers-API (gids 2026)

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

Als je een Vue-valutaconverter nodig hebt die realtime wisselkoersen ophaalt van een live API, dan loopt deze tutorial je er van begin tot eind doorheen. Je bouwt een productieklaar Vue 3-component met de Composition API, TypeScript en een gratis wisselkoers-API — met goede debouncing, caching, foutafhandeling en SSR-veilige patronen voor Nuxt.

Aan het einde heb je een herbruikbaar <CurrencyConverter />-component, een useCurrencyRates-composable die je in elk Vue 3-project kunt droppen, en een helder begrip van de afwegingen die ertoe doen wanneer je valutaconversie naar echte gebruikers brengt.

Wat je gaat bouwen

Een Vue 3-valutaconverter met deze functies:

  • Live wisselkoersen voor 170+ valuta's via de Finexly API
  • Reactieve conversie die meeloopt terwijl de gebruiker typt
  • Wisselknop voor valuta's (USD → EUR wordt met één klik EUR → USD)
  • Debounced API-calls zodat het netwerk niet wordt overbelast
  • In-memory cache om de latency laag te houden en binnen het gratis quotum te blijven
  • Foutafhandeling, loading-toestanden en nette TypeScript-types
  • SSR-vriendelijk zodat het in Nuxt 3 werkt zonder hydration-mismatches

Het uiteindelijke component is zo'n 80 regels code. De composable nog 60. Meer is het niet.

Vereisten

Je moet vertrouwd zijn met:

  • Vue 3-syntax (de <script setup>-vorm)
  • Basis-TypeScript
  • REST API's aanroepen met fetch

Je hebt ook een Finexly-API-sleutel nodig. Pak er een vanuit het dashboard — duurt zo'n 30 seconden, en het gratis abonnement geeft je 1.000 requests per maand zonder creditcard. Heb je de service nog nooit gebruikt? In de Finexly-API-documentatie vind je een quickstart van 5 minuten.

Stap 1: Een Vue 3-project opzetten met Vite

Als je vanaf nul start:

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

Vite levert Vue 3, TypeScript en hot module reload kant-en-klaar. Open src/App.vue en gooi de boilerplate weg — die vervangen we zo door de converter.

Integreer je in een bestaand Nuxt 3-project, dan kun je deze stap overslaan. De composable hieronder werkt in Nuxt identiek omdat hij standaard ref en computed uit vue gebruikt.

Stap 2: De API-sleutel veilig opslaan

Plak je Finexly-API-sleutel nooit direct in een component. Zet hem in .env.local:

VITE_FINEXLY_API_KEY=jouw_sleutel_hier

Vite stelt elke variabele met VITE_-prefix beschikbaar aan de client. Voor Nuxt gebruik je NUXT_PUBLIC_FINEXLY_API_KEY en lees je hem uit useRuntimeConfig().public.finexlyApiKey.

Als je je zorgen maakt dat de sleutel in de client-bundle terechtkomt, proxy het verzoek dan via een backend-route of een serverless function. Dat patroon laten we zien in stap 7.

Stap 3: De useCurrencyRates-composable bouwen

De composable is het hart van de converter. Hij regelt fetch, cache en loading-/error-state — zodat het component puur presentationeel blijft.

Maak src/composables/useCurrencyRates.ts aan:

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 op moduleniveau: gedeeld tussen alle componenten die deze composable aanroepen.
// Elke entry leeft 5 minuten — lang genoeg om instant aan te voelen, kort genoeg
// om accuraat te blijven bij volatiele FX-bewegingen.
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 }
}

Een paar details om te onthouden:

  • De cache leeft bewust op moduleniveau. Twee componenten die de composable aanroepen delen dezelfde cache, dus van route wisselen leidt niet tot een nieuwe fetch voor dezelfde basisvaluta.
  • rates is getypeerd als Record<string, number> zodat je hem direct in een <select> kunt voeden.
  • De convert-functie doet kruiskoers-rekenen, dus je hoeft niet opnieuw te fetchen telkens als de "from"-valuta verandert. Meer hierover in stap 5.

Stap 4: Het converter-component

Maak 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>

Drop <CurrencyConverter /> in App.vue en je hebt een werkende converter. De lijst met valuta's vult zich zodra de eerste fetch terugkomt. Tikken in het bedrag-veld werkt het resultaat reactief bij zonder het netwerk aan te raken. De wisselknop zet de dropdowns om. De "from"-valuta veranderen triggert een debounced refetch.

Stap 5: Kruiskoersconversie (om geen requests te verspillen)

Als je alleen vanuit één basisvaluta converteert, kun je deze sectie overslaan. Maar de meeste converters laten de gebruiker beide kanten kiezen — en een naïeve implementatie haalt opnieuw op bij elke "from"-wijziging.

Kruiskoersrekenen lost dat op. Heb je USD als basis en weet je de koers van EUR en JPY tegen USD, dan is de EUR → JPY-koers simpelweg (1 / EUR_rate) * JPY_rate. Precies wat de convert-functie uit stap 3 doet:

return (amount / fromRate) * toRate

Dat betekent dat één fetch per sessie genoeg is. Grote winst voor het gratis quotum. De watcher op from wordt een verdedigingsmaatregel — kiest een gebruiker een exotische valuta die niet in de gecachte tabel staat, dan zorgt opnieuw fetchen met die basis dat de conversie blijft werken.

Stap 6: Loading-skeletons en foutstaten

Spinners zijn prima, maar een layout shift terwijl koersen laden is irritant. Render placeholder-opties bij de eerste 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>

Toon op het foutpad een retry-knop in plaats van een doodlopende boodschap:

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

Behandel 429 (rate limited) en 5xx apart als je beleefd wilt zijn. Met de 1.000 gratis maandelijkse requests van Finexly en 5 minuten cache heb je een echte piek nodig om de limiet te raken — maar een nette retry-flow maakt de UI solide.

Stap 7: Verberg je API-sleutel achter een serverproxy

Alles in import.meta.env.VITE_* belandt in de client-bundle. Voor de meeste read-only valutawidgets is dat acceptabel — slechtste geval is dat iemand met je sleutel koersen scrapet. Wil je defense-in-depth, proxy het verzoek dan server-side.

Met een Nuxt 3-server-route:

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

Wijs de composable vervolgens naar /api/rates?base=${base} in plaats van de Finexly-origin. De sleutel verlaat de server nooit. Dezelfde aanpak werkt in elk Vue-meta-framework — Nuxt, Quasar of een simpele Express-backend.

Stap 8: Productie-checklist

Voor je live gaat:

  • Cache agressief. FX-koersen veranderen niet elke seconde. Een TTL van 5 minuten op de client en 1 minuut op de server is meer dan genoeg voor bijna elk weergavescenario. Zie onze caching- en error-handling-gids voor in productie geteste patronen.
  • Rond correct af. Valutaweergave rondt af op de kleinste eenheid van de valuta (2 decimalen voor USD, 0 voor JPY, 3 voor KWD). Gebruik Intl.NumberFormat(locale, { style: 'currency', currency: to.value }) in plaats van .toFixed(2).
  • Format met Intl. new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(123.45) geeft "€123.45" en respecteert de locale van de gebruiker.
  • SSR-veilige fetches. Geef in Nuxt de voorkeur aan useFetch boven een rauwe fetch in onMounted, zodat koersen tijdens server-render beschikbaar zijn en geen hydration-mismatch veroorzaken.
  • Bewaak je quotum. Voeg een kleine logger toe die waarschuwt zodra je 80% van het maandelijkse request-budget hebt gebruikt. Voor hoger volume beginnen de prijsabonnementen waar het gratis abonnement eindigt.

Vue 2 (Options API)-variant

Zit je op Vue 2? Dezelfde logica vertaalt vrijwel regel voor regel:

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

De Composition API is netter voor het delen van logica tussen componenten, maar de Options API werkt prima voor één enkele converter-widget. Heeft je team voorkeur voor een meer functionele aanpak, dan dekt de JavaScript-integratiegids framework-agnostische patronen.

Veelvoorkomende valkuilen (en hoe je ze vermijdt)

Hydration-mismatch in Nuxt. fetch aanroepen in onMounted werkt client-side maar breekt de SSR-consistentie. Gebruik in plaats daarvan useFetch of useAsyncData.

Verouderde koersen na een lange sessie. De TTL van 5 minuten betekent dat een tab die de hele dag openstaat koersen van een uur geleden toont. Vernieuw via visibilitychange:

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

Floating-point-drift. JavaScript-getallen zijn doubles. 0.1 + 0.2 !== 0.3. Vermenigvuldig voor geldbedragen naar gehele centen (amount * 100), reken, en deel daarna terug. Of gebruik een library als dinero.js voor alles wat met checkout te maken heeft.

CORS-fouten in development. Sommige valuta-API's verbieden directe browser-calls. Finexly staat browser-origins toe voor client-side gebruik; de proxy uit stap 7 lost de rest op.

Waarom Finexly voor Vue-projecten

Een paar dingen tellen bij het kiezen van een wisselkoers-API voor een frontend-app: responstijd, nauwkeurigheid, gulheid van het gratis abonnement en schone JSON. Finexly mikt op alle vier — sub-50ms p95-latency, mid-market-koersen die elke 60 seconden verversen, 170+ valuta's en een JSON-vorm die zonder bewerking direct in een Vue-ref valt.

Wil je weten hoe het zich verhoudt tot alternatieven, dan loopt onze valuta-API-vergelijking de afwegingen in detail door. Of gebruik de side-by-side-vergelijkingspagina.

Veelgestelde vragen

Kan ik deze tutorial met Vue 2 gebruiken?

Ja. Het composable-patroon is alleen voor Vue 3, maar de onderliggende logica — koersen ophalen, opslaan in data, conversie berekenen — werkt identiek in de Options API. Het Vue 2-voorbeeld hierboven is een directe vervanger.

Is de Finexly-API gratis voor Vue-projecten?

Het gratis abonnement geeft je 1.000 requests per maand, ruim genoeg voor een sideproject, een portfolio-stuk of een kleine SaaS. Met cache van 5 minuten kun je daarmee zo'n 200 dagelijks actieve gebruikers ondersteunen. Voor hogere volumes zie de prijsabonnementen.

Hoe voorkom ik dat mijn API-sleutel in de client-bundle eindigt?

Proxy het verzoek via een server-route zoals in stap 7. De prefixes VITE_* en NUXT_PUBLIC_* maken variabelen beide zichtbaar voor de client. Alles wat gevoelig is hoort achter een server-functie.

Hoe nauwkeurig zijn de koersen?

Finexly aggregeert mid-market-koersen van meerdere Tier-1-liquidity-providers en ververst elke 60 seconden. Nauwkeurig genoeg voor weergave en de meeste pricing-toepassingen. Voor trade-uitvoering wil je een streaming-feed — zie onze REST vs WebSocket-gids.

Kan ik historische bedragen converteren?

Ja — het /historical-endpoint van Finexly accepteert een datumparameter en geeft koersen terug van elke werkdag. Het patroon is identiek aan het /latest-endpoint hierboven; alleen de URL wisselen. De historische-wisselkoersen-API-gids behandelt dit uitgebreid.

Afronding

Je hebt nu een Vue 3-valutaconverter die echte productiezorgen aankan — caching, debouncing, error-states, SSR en API-sleutelveiligheid. Dankzij het composable-patroon kun je dezelfde useCurrencyRates zo in een navbar-widget, checkout-pagina of dashboardgrafiek droppen, zonder de fetch-logica opnieuw te hoeven schrijven.

Klaar om het in je eigen project te proberen? Pak je gratis Finexly-API-sleutel — geen creditcard nodig. Begin met 1.000 gratis requests per maand en upgrade pas wanneer je verkeer over het gratis abonnement heen groeit.

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 →