Zurück zum Blog

So baust du einen Währungsrechner mit Vue.js und einer Live-Wechselkurs-API (2026 Guide)

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

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 dev

Vite 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_hier

Vite 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 cache lebt 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.
  • rates ist als Record<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) * toRate

Das 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 Intl formatieren. 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 useFetch statt einem rohen fetch in onMounted, 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.

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 →

Diesen Artikel teilen