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 devVite 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_hierVite 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
cacheleeft 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. ratesis getypeerd alsRecord<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) * toRateDat 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
useFetchboven een rauwefetchinonMounted, 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.
Explore More
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 →