Back to Documentation

Vue.js Integration Guide

Learn how to integrate FXRateSync API with Vue.js using the Composition API and best practices.

Composable for Currency Operations

Create a reusable Vue composable for currency conversion:

// composables/useCurrencyConverter.js
import { ref, reactive } from 'vue'

export function useCurrencyConverter() {
  const loading = ref(false)
  const error = ref(null)
  const result = ref(null)

  const convert = async (from, to, amount) => {
    loading.value = true
    error.value = null
    result.value = null

    try {
      const response = await fetch(
        `https://api.fxratesync.io/v1/convert?from=${from}&to=${to}&amount=${amount}`,
        {
          headers: {
            'X-API-Key': process.env.NEXT_PUBLIC_FX_API_KEY
          }
        }
      )

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      const data = await response.json()
      result.value = data
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  const reset = () => {
    result.value = null
    error.value = null
  }

  return {
    loading: readonly(loading),
    error: readonly(error),
    result: readonly(result),
    convert,
    reset
  }
}

Vue Component Example

<!-- CurrencyConverter.vue -->
<template>
  <div class="max-w-md mx-auto p-6 bg-background rounded-xl shadow-lg">
    <h2 class="text-2xl font-bold text-center mb-6">Currency Converter</h2>
    
    <form @submit.prevent="handleSubmit" class="space-y-4">
      <!-- Amount Input -->
      <div>
        <label for="amount" class="block text-sm font-medium text-foreground mb-2">
          Amount
        </label>
        <input
          id="amount"
          v-model.number="form.amount"
          type="number"
          min="0"
          step="0.01"
          class="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
          placeholder="Enter amount"
          required
        />
      </div>

      <!-- Currency Selectors -->
      <div class="grid grid-cols-2 gap-4">
        <div>
          <label for="from" class="block text-sm font-medium text-foreground mb-2">
            From
          </label>
          <select
            id="from"
            v-model="form.from"
            class="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
          >
            <option v-for="currency in currencies" :key="currency" :value="currency">
              {{ currency }}
            </option>
          </select>
        </div>

        <div>
          <label for="to" class="block text-sm font-medium text-foreground mb-2">
            To
          </label>
          <select
            id="to"
            v-model="form.to"
            class="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
          >
            <option v-for="currency in currencies" :key="currency" :value="currency">
              {{ currency }}
            </option>
          </select>
        </div>
      </div>

      <!-- Swap Button -->
      <div class="text-center">
        <button
          type="button"
          @click="swapCurrencies"
          class="px-4 py-2 text-primary hover:bg-primary/10 rounded-lg transition-colors"
        >
          ⇄ Swap Currencies
        </button>
      </div>

      <!-- Submit Button -->
      <button
        type="submit"
        :disabled="loading"
        class="w-full bg-primary hover:bg-primary/90 disabled:opacity-50 text-background font-medium py-3 px-6 rounded-lg transition-colors"
      >
        {{ loading ? 'Converting...' : 'Convert Currency' }}
      </button>
    </form>

    <!-- Results -->
    <div v-if="result" class="mt-6 p-4 bg-success/10 border border-success/30 rounded-lg">
      <div class="text-center">
        <div class="text-2xl font-bold text-success-foreground mb-1">
          {{ formatCurrency(result.converted_amount, result.to) }}
        </div>
        <div class="text-sm text-success">
          {{ result.amount }} {{ result.from }} = 
          {{ result.converted_amount.toFixed(2) }} {{ result.to }}
        </div>
        <div class="text-xs text-success mt-2">
          Rate: {{ result.rate.toFixed(4) }} • 
          {{ new Date(result.timestamp).toLocaleString() }}
        </div>
      </div>
    </div>

    <!-- Error Display -->
    <div v-if="error" class="mt-6 p-4 bg-destructive/10 border border-destructive/30 rounded-lg">
      <div class="text-destructive-foreground text-center">
        {{ error }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { reactive } from 'vue'
import { useCurrencyConverter } from '@/composables/useCurrencyConverter'

const { loading, error, result, convert, reset } = useCurrencyConverter()

const currencies = ['USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'CHF', 'CNY']

const form = reactive({
  amount: 100,
  from: 'USD',
  to: 'EUR'
})

const handleSubmit = async () => {
  if (form.amount <= 0) {
    return
  }
  
  await convert(form.from, form.to, form.amount)
}

const swapCurrencies = () => {
  const temp = form.from
  form.from = form.to
  form.to = temp
  reset()
}

const formatCurrency = (amount, currency) => {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency,
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(amount)
}
</script>

Pinia Store for State Management

// stores/currency.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCurrencyStore = defineStore('currency', () => {
  // State
  const rates = ref({})
  const lastUpdated = ref(null)
  const baseCurrency = ref('USD')
  const loading = ref(false)
  const error = ref(null)

  // Getters
  const isDataFresh = computed(() => {
    if (!lastUpdated.value) return false
    const fiveMinutesAgo = Date.now() - 5 * 60 * 1000
    return new Date(lastUpdated.value).getTime() > fiveMinutesAgo
  })

  const getRate = computed(() => {
    return (from, to) => {
      if (from === to) return 1
      
      // Direct rate available
      if (rates.value[to]) {
        return rates.value[to]
      }
      
      // Calculate cross rate
      const fromRate = rates.value[from]
      const toRate = rates.value[to]
      
      if (fromRate && toRate) {
        return toRate / fromRate
      }
      
      return null
    }
  })

  // Actions
  const fetchRates = async (base = 'USD') => {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(
        `https://api.fxratesync.io/v1/rates/${base}`,
        {
          headers: {
            'X-API-Key': process.env.NEXT_PUBLIC_FX_API_KEY
          }
        }
      )

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      const data = await response.json()
      rates.value = data.rates
      lastUpdated.value = data.timestamp
      baseCurrency.value = base
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  const convertAmount = (amount, from, to) => {
    const rate = getRate.value(from, to)
    return rate ? amount * rate : null
  }

  return {
    // State
    rates,
    lastUpdated,
    baseCurrency,
    loading,
    error,
    
    // Getters
    isDataFresh,
    getRate,
    
    // Actions
    fetchRates,
    convertAmount
  }
})

Best Practices

  • • Use environment variables for API keys (NEXT_PUBLIC_FX_API_KEY)
  • • Implement proper error boundaries
  • • Cache data in Pinia for better performance
  • • Use Vue's reactivity system effectively
  • • Consider using vue-query for advanced data fetching
  • • Implement proper TypeScript types for better DX

Environment Setup

.env File

NEXT_PUBLIC_FX_API_KEY=your_api_key_here

Explore More

Continue learning with other framework integration guides