Wenn du einen Vue-Währungsrechner brauchst, der Echtzeit-Wechselkurse von einer Live-API zieht, führt dich dieses Tutorial Schritt für Schritt durch. Du baust eine produktionsreife Vue-3-Komponente mit der Composition API, TypeScript und einer kostenlosen Wechselkurs-API — mit Debouncing, Caching, Fehlerbehandlung und SSR-tauglichen Patterns für Nuxt.
Am Ende hast du eine wiederverwendbare <CurrencyConverter />-Komponente, ein useCurrencyRates-Composable, das du in jedes Vue-3-Projekt einbauen kannst, und ein klares Verständnis der Trade-offs, die zählen, wenn du Währungsumrechnung an echte Nutzer ausspielst.
Was du baust
Ein Vue-3-Währungsrechner mit diesen Features:
- Live-Wechselkurse für 170+ Währungen über die Finexly-API
- Reaktive Umrechnung, die sich beim Tippen aktualisiert
- Tausch-Button (USD → EUR wird mit einem Klick zu EUR → USD)
- Debouncte API-Calls, damit das Netz nicht zugespammt wird
- In-Memory-Cache, um Latenz niedrig zu halten und im Free-Tier-Quota zu bleiben
- Fehlerbehandlung, Ladezustände und saubere TypeScript-Typen
- SSR-freundlich, läuft in Nuxt 3 ohne Hydration-Mismatches
Die finale Komponente hat etwa 80 Zeilen Code. Das Composable weitere 60. Mehr nicht.
Voraussetzungen
Du solltest dich auskennen mit:
- Vue-3-Syntax (der
<script setup>-Form) - Grundlegendem TypeScript
- REST-API-Aufrufen mit
fetch
Du brauchst auch einen Finexly-API-Key. Hol dir einen vom Dashboard — dauert etwa 30 Sekunden, der Free-Plan gibt dir 1.000 Requests pro Monat ohne Kreditkarte. Falls du den Service noch nicht kennst: die Finexly-API-Dokumentation hat einen 5-Minuten-Quickstart.
Schritt 1: Vue-3-Projekt mit Vite aufsetzen
Wenn du frisch startest:
npm create vite@latest finexly-converter -- --template vue-ts
cd finexly-converter
npm install
npm run devVite gibt dir Vue 3, TypeScript und Hot Module Reload out of the box. Öffne src/App.vue und lösche das Boilerplate — wir ersetzen es gleich durch den Konverter.
Wenn du in ein bestehendes Nuxt-3-Projekt integrierst, kannst du diesen Schritt überspringen. Das Composable unten funktioniert in Nuxt identisch, weil es Standard-ref und computed aus vue nutzt.
Schritt 2: API-Key sicher ablegen
Klebe deinen Finexly-API-Key niemals direkt in eine Komponente. Pack ihn in .env.local:
VITE_FINEXLY_API_KEY=dein_key_hierVite macht jede mit VITE_ präfixierte Variable für den Client sichtbar. Bei Nuxt nutze NUXT_PUBLIC_FINEXLY_API_KEY und lies sie aus useRuntimeConfig().public.finexlyApiKey.
Wenn dich das Aussetzen des Keys im Client-Bundle stört, leite die Anfrage über eine Backend-Route oder Serverless-Function. Wir zeigen dieses Pattern in Schritt 7.
Schritt 3: Das useCurrencyRates-Composable bauen
Das Composable ist das Herz des Konverters. Es kümmert sich um Fetch, Cache und Loading/Error-State — damit die Komponente präsentational bleibt.
Lege src/composables/useCurrencyRates.ts an:
import { ref, type Ref } from 'vue'
const API_KEY = import.meta.env.VITE_FINEXLY_API_KEY
const BASE_URL = 'https://api.finexly.com/v1'
// Modul-Level-Cache: zwischen allen Komponenten geteilt, die dieses Composable aufrufen.
// Jeder Eintrag lebt 5 Minuten — lange genug, um instant zu wirken, kurz genug, um
// in volatilen FX-Phasen genau zu bleiben.
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 }
}Ein paar Details, die wichtig sind:
- Der
cachelebt absichtlich auf Modul-Ebene. Zwei Komponenten, die das Composable aufrufen, teilen sich denselben Cache, sodass ein Routenwechsel die gleiche Basiswährung nicht erneut lädt. ratesist alsRecord<string, number>typisiert, damit du ihn direkt in ein<select>füttern kannst.- Die
convert-Funktion macht Cross-Rate-Mathematik, sodass du nicht jedes Mal neu fetchen musst, wenn die "from"-Währung wechselt. Mehr dazu in Schritt 5.
Schritt 4: Die Konverter-Komponente
Lege src/components/CurrencyConverter.vue an:
<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>Pack <CurrencyConverter /> in App.vue und du hast einen funktionierenden Konverter. Die Währungsliste füllt sich, sobald der erste Fetch zurückkommt. Tippen ins Betragsfeld aktualisiert das Ergebnis reaktiv ohne Netzwerkzugriff. Der Tausch-Button vertauscht die Dropdowns. Ein Wechsel der "from"-Währung löst einen debouncten Refetch aus.
Schritt 5: Cross-Rate-Konvertierung (damit du keine Requests verbrennst)
Wenn du nur von einer Basis aus konvertierst, kannst du diesen Abschnitt überspringen. Aber die meisten Konverter lassen Nutzer beide Seiten wählen — und eine naive Implementierung fetcht bei jedem "from"-Wechsel.
Cross-Rate-Mathematik löst das. Wenn du USD als Basis hast und die EUR- und JPY-Kurse zu USD kennst, ist der EUR → JPY-Kurs einfach (1 / EUR_rate) * JPY_rate. Genau das macht die convert-Funktion aus Schritt 3:
return (amount / fromRate) * toRateDas heißt: du musst pro Session nur einmal fetchen. Großer Gewinn fürs Free-Tier-Quota. Der Watcher auf from wird zur Sicherheitsmaßnahme — falls ein Nutzer eine exotische Währung wählt, die nicht in der gecachten Tabelle ist, garantiert ein Refetch mit dieser Basis, dass die Umrechnung weiter funktioniert.
Schritt 6: Loading-Skeletons und Fehlerzustände
Spinner sind okay, aber ein Layout-Shift während die Kurse laden, ist nervig. Render Placeholder-Optionen beim ersten 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>Im Fehlerpfad zeig einen Retry-Button statt einer Sackgassen-Meldung:
<div v-if="error" class="error-box">
<p>{{ error }}</p>
<button @click="fetchRates(from)">Retry</button>
</div>Behandle 429 (Rate-Limit) und 5xx getrennt, wenn du höflich sein willst. Mit Finexlys 1.000 Free-Requests pro Monat und 5-Minuten-Cache bräuchtest du einen echten Spike, um das Limit zu erreichen — aber ein sauberer Retry-Pfad lässt die UI solide wirken.
Schritt 7: API-Key hinter einem Server-Proxy verstecken
Alles in import.meta.env.VITE_* landet im Client-Bundle. Für die meisten Read-only-Währungswidgets ist das akzeptabel — der Worst Case ist, dass jemand mit deinem Key Kurse abgreift. Wenn du Defense in Depth willst, leite die Anfrage über den Server.
Mit einer 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}`
)
})Dann zeig das Composable auf /api/rates?base=${base} statt auf den Finexly-Origin. Der Key verlässt nie den Server. Das gleiche Pattern funktioniert in jedem Vue-Meta-Framework — Nuxt, Quasar oder ein einfaches Express-Backend.
Schritt 8: Production-Checkliste
Bevor du deployst:
- Aggressiv cachen. FX-Kurse ändern sich nicht jede Sekunde. 5-Minuten-TTL im Client und 1-Minuten-TTL im Server reichen für fast jeden Display-Use-Case. Siehe unseren Caching- und Error-Handling-Guide für produktionserprobte Patterns.
- Korrekt runden. Währungsanzeige rundet auf die kleinste Einheit der Währung (2 Stellen für USD, 0 für JPY, 3 für KWD). Nutze
Intl.NumberFormat(locale, { style: 'currency', currency: to.value })statt.toFixed(2). - Mit
Intlformatieren.new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(123.45)liefert „€123.45" und respektiert die Locale des Nutzers. - SSR-sichere Fetches. In Nuxt nimm
useFetchstatt einem rohenfetchinonMounted, damit die Kurse beim Server-Rendering verfügbar sind und keinen Hydration-Mismatch auslösen. - Quota überwachen. Bau einen kleinen Logger ein, der warnt, wenn 80 % des monatlichen Request-Budgets verbraucht sind. Für höheres Volumen starten die Preispläne, wo der Free-Tier endet.
Vue-2-Variante (Options API)
Wenn du auf Vue 2 bist, übersetzt sich dieselbe Logik fast Zeile für Zeile:
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
}
},
},
}Die Composition API ist sauberer für Logik-Sharing zwischen Komponenten, aber die Options API funktioniert für ein einzelnes Konverter-Widget gut. Wenn dein Team eher funktional unterwegs ist, deckt der JavaScript-Integrationsguide framework-agnostische Patterns ab.
Häufige Fallstricke (und wie du sie vermeidest)
Hydration-Mismatch in Nuxt. fetch in onMounted aufzurufen funktioniert clientseitig, bricht aber die SSR-Konsistenz. Nutze stattdessen useFetch oder useAsyncData.
Veraltete Kurse nach langer Session. Die 5-Minuten-TTL bedeutet, dass ein den ganzen Tag offener Tab Kurse aus der letzten Stunde anzeigt. Refresh über visibilitychange:
document.addEventListener('visibilitychange', () => {
if (!document.hidden) fetchRates(from.value)
})Floating-Point-Drift. JavaScript-Zahlen sind Doubles. 0.1 + 0.2 !== 0.3. Für Geldbeträge: zu Cent-Integern multiplizieren (amount * 100), rechnen, dann teilen. Oder eine Library wie dinero.js für alles Checkout-Relevante.
CORS-Fehler in der Entwicklung. Manche Currency-APIs verbieten direkte Browser-Calls. Finexly erlaubt Browser-Origins für client-seitigen Einsatz; der Proxy aus Schritt 7 löst die anderen Fälle.
Warum Finexly für Vue-Projekte
Ein paar Dinge zählen bei der Wahl einer Wechselkurs-API für eine Frontend-App: Antwortzeit, Genauigkeit, Großzügigkeit des Free-Tiers und sauberes JSON. Finexly zielt auf alle vier — sub-50ms p95-Latenz, alle 60 Sekunden aktualisierte Mid-Market-Kurse, 170+ Währungen und ein JSON-Format, das ohne Massieren direkt in eine Vue-ref fällt.
Wenn du sehen willst, wie es sich gegen Alternativen schlägt, geht unsere Currency-API-Vergleichsseite die Trade-offs im Detail durch. Oder nimm die Side-by-Side-Vergleichsseite.
Häufig gestellte Fragen
Kann ich dieses Tutorial mit Vue 2 nutzen?
Ja. Das Composable-Pattern ist Vue-3-only, aber die zugrundeliegende Logik — Kurse fetchen, in data ablegen, Umrechnung berechnen — funktioniert in der Options API identisch. Das Vue-2-Beispiel oben ist ein direkter Ersatz.
Ist die Finexly-API für Vue-Projekte kostenlos?
Der Free-Plan gibt dir 1.000 Requests pro Monat, mehr als genug für ein Side-Project, ein Portfolio-Stück oder ein kleines SaaS. Mit 5-Minuten-Cache trägt das etwa 200 tägliche aktive Nutzer. Siehe Preispläne für höhere Volumen.
Wie verhindere ich, dass mein API-Key im Client-Bundle landet?
Leite die Anfrage über eine Server-Route wie in Schritt 7. Sowohl VITE_* als auch NUXT_PUBLIC_* machen Variablen client-sichtbar. Alles Sensible gehört hinter eine Server-Function.
Wie genau sind die Kurse?
Finexly aggregiert Mid-Market-Kurse von mehreren Tier-1-Liquiditätsanbietern und refresht alle 60 Sekunden. Genau genug für Anzeige und die meisten Pricing-Anwendungen. Für Trade-Ausführung willst du einen Streaming-Feed — siehe unseren REST-vs-WebSocket-Guide.
Kann ich historische Beträge umrechnen?
Ja — Finexlys /historical-Endpoint nimmt einen Date-Parameter und liefert Kurse für jeden Geschäftstag. Das Pattern ist identisch zum /latest-Endpoint oben; nur die URL austauschen. Der Historische-Wechselkurs-API-Guide deckt es im Detail ab.
Fazit
Du hast jetzt einen Vue-3-Währungsrechner, der reale Anliegen abdeckt — Caching, Debouncing, Fehlerzustände, SSR und API-Key-Sicherheit. Das Composable-Pattern bedeutet, dass du dasselbe useCurrencyRates in ein Navbar-Widget, eine Checkout-Seite oder ein Dashboard-Diagramm packen kannst, ohne die Fetch-Logik neu zu schreiben.
Bereit, es im eigenen Projekt zu testen? Hol dir deinen kostenlosen Finexly-API-Key — keine Kreditkarte nötig. Starte mit 1.000 freien Requests pro Monat und upgrade erst, wenn dein Traffic über den Free-Tier hinauswächst.
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 →