Si necesitas un conversor de divisas en Vue que obtenga tipos de cambio en tiempo real desde una API en vivo, este tutorial te guía de principio a fin. Construirás un componente Vue 3 listo para producción usando la Composition API, TypeScript y una API gratuita de tipos de cambio — con debouncing adecuado, caché, manejo de errores y patrones compatibles con SSR para Nuxt.
Al final tendrás un componente reutilizable <CurrencyConverter />, un composable useCurrencyRates que puedes integrar en cualquier proyecto Vue 3, y una comprensión clara de los compromisos que importan al lanzar conversión de divisas a usuarios reales.
Lo que vas a construir
Un conversor de divisas Vue 3 con estas funcionalidades:
- Tipos de cambio en vivo para 170+ divisas vía la API de Finexly
- Conversión reactiva que se actualiza mientras el usuario escribe
- Botón de intercambio de divisas (USD → EUR se convierte en EUR → USD con un clic)
- Llamadas a la API con debouncing para no saturar la red
- Caché en memoria para mantener baja la latencia y respetar las cuotas del plan gratuito
- Manejo de errores, estados de carga y tipos TypeScript adecuados
- Compatible con SSR para que funcione en Nuxt 3 sin desajustes de hidratación
El componente final tiene unas 80 líneas de código. El composable, otras 60. Eso es todo.
Requisitos previos
Deberías estar cómodo con:
- Sintaxis de Vue 3 (la forma
<script setup>) - TypeScript básico
- Llamar a una API REST con
fetch
También necesitarás una clave API de Finexly. Consíguela en el panel — toma unos 30 segundos y el plan gratuito ofrece 1.000 peticiones al mes sin tarjeta de crédito. Si nunca has usado el servicio, la documentación de la API de Finexly tiene un quickstart de 5 minutos.
Paso 1: Crear un proyecto Vue 3 con Vite
Si empiezas desde cero:
npm create vite@latest finexly-converter -- --template vue-ts
cd finexly-converter
npm install
npm run devVite te da Vue 3, TypeScript y hot module reload listos para usar. Abre src/App.vue y borra el código boilerplate — lo reemplazaremos con el conversor en breve.
Si lo integras en un proyecto Nuxt 3 existente, puedes saltarte este paso. El composable de abajo funciona idéntico en Nuxt porque usa ref y computed estándar de vue.
Paso 2: Almacenar la clave API de forma segura
Nunca pegues tu clave API de Finexly directamente en un componente. Ponla en .env.local:
VITE_FINEXLY_API_KEY=tu_clave_aquiVite expone cualquier variable con prefijo VITE_ al cliente. Para Nuxt, usa NUXT_PUBLIC_FINEXLY_API_KEY y léela desde useRuntimeConfig().public.finexlyApiKey.
Si te preocupa exponer la clave en el bundle del cliente, redirige la petición a través de una ruta backend o función serverless. Mostraremos ese patrón en el Paso 7.
Paso 3: Construir el composable useCurrencyRates
El composable es el corazón del conversor. Maneja la petición, la caché y el estado de carga/error — para que el componente quede presentacional.
Crea src/composables/useCurrencyRates.ts:
import { ref, type Ref } from 'vue'
const API_KEY = import.meta.env.VITE_FINEXLY_API_KEY
const BASE_URL = 'https://api.finexly.com/v1'
// Caché a nivel de módulo: compartida entre todos los componentes que llaman este composable.
// Cada entrada vive 5 minutos — suficiente para sentirse instantáneo, lo bastante corto
// para mantenerse preciso durante movimientos volátiles del FX.
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 }
}Algunos detalles a destacar:
- La
cachevive a nivel de módulo a propósito. Dos componentes que llaman al composable comparten la misma caché, lo que significa que cambiar entre rutas no volverá a pedir la misma divisa base. ratesestá tipado comoRecord<string, number>para que puedas alimentar directamente un<select>más tarde.- La función
converthace cálculo de tipos cruzados, así no tienes que recargar cada vez que cambia la divisa "from". Más sobre esto en el Paso 5.
Paso 4: El componente conversor
Crea 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>Coloca <CurrencyConverter /> en App.vue y tendrás un conversor funcional. La lista de divisas se rellena en cuanto llega la primera petición. Escribir en el campo de cantidad actualiza el resultado de forma reactiva sin tocar la red. Intercambiar divisas reordena los desplegables. Cambiar la divisa "from" dispara una recarga con debouncing.
Paso 5: Conversión por tipo cruzado (para no quemar peticiones)
Si solo conviertes desde una sola base, puedes saltarte esta sección. Pero la mayoría de conversores permiten al usuario elegir ambos lados — y una implementación ingenua recarga con cada cambio del "from".
La matemática de tipos cruzados lo soluciona. Si tienes USD como base y los tipos de EUR y JPY en términos de USD, el tipo EUR → JPY es simplemente (1 / EUR_rate) * JPY_rate. Eso es exactamente lo que hace la función convert del Paso 3:
return (amount / fromRate) * toRateEsto significa que solo necesitas hacer una petición por sesión. Una gran victoria para las cuotas del plan gratuito. El watcher sobre from se vuelve una medida defensiva — si un usuario elige una divisa exótica que no está en la tabla cacheada, recargar con esa base garantiza que la conversión siga funcionando.
Paso 6: Skeletons de carga y estados de error
Los spinners están bien, pero un cambio de layout mientras cargan los tipos es molesto. Renderiza opciones placeholder en el primer 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>Para el camino de error, muestra un botón de reintentar en lugar de un mensaje sin salida:
<div v-if="error" class="error-box">
<p>{{ error }}</p>
<button @click="fetchRates(from)">Retry</button>
</div>Trata 429 (rate limited) y 5xx por separado si quieres ser educado. Con las 1.000 peticiones gratuitas mensuales de Finexly y caché de 5 minutos, necesitarías un pico real para alcanzar el límite — pero un camino de retry limpio hace que la UI se sienta sólida.
Paso 7: Ocultar tu clave API con un proxy de servidor
Cualquier cosa en import.meta.env.VITE_* termina en el bundle del cliente. Para la mayoría de widgets de divisas de solo lectura es aceptable — el peor caso es que alguien raspe tipos con tu clave. Si quieres defensa en profundidad, redirige la petición desde el servidor.
Con una ruta de servidor de Nuxt 3:
// 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}`
)
})Después apunta el composable a /api/rates?base=${base} en lugar de al origen de Finexly. La clave nunca sale del servidor. El mismo patrón funciona en cualquier meta-framework de Vue — Nuxt, Quasar o un backend Express plano.
Paso 8: Lista de verificación de producción
Antes de publicar:
- Cachea agresivamente. Los tipos FX no cambian cada segundo. Un TTL de 5 minutos en el cliente y 1 minuto en el servidor es más que suficiente para casi cualquier caso de visualización. Consulta nuestra guía de caché y manejo de errores para patrones probados en producción.
- Redondea correctamente. La visualización de divisas redondea a la unidad menor de la divisa (2 decimales para USD, 0 para JPY, 3 para KWD). Usa
Intl.NumberFormat(locale, { style: 'currency', currency: to.value })en lugar de.toFixed(2). - Formatea con
Intl.new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(123.45)te da "€123.45" y respeta la localización del usuario. - Fetches seguros para SSR. En Nuxt, prefiere
useFetchen lugar de unfetchcrudo dentro deonMountedpara que los tipos estén disponibles durante el render del servidor y no provoquen un desajuste de hidratación. - Monitoriza tu cuota. Añade un pequeño logger que avise cuando hayas consumido el 80% del presupuesto mensual de peticiones. Para volumen mayor, los planes de precios empiezan donde termina el plan gratuito.
Variante Vue 2 (Options API)
Si estás en Vue 2, la misma lógica se traduce casi línea a línea:
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
}
},
},
}La Composition API es más limpia para compartir lógica entre componentes, pero la Options API funciona bien para un único widget conversor. Si tu equipo prefiere un enfoque más funcional, la guía de integración con JavaScript cubre patrones agnósticos al framework.
Errores comunes (y cómo evitarlos)
Desajuste de hidratación en Nuxt. Llamar a fetch en onMounted funciona en cliente pero rompe la consistencia SSR. Usa useFetch o useAsyncData en su lugar.
Tipos obsoletos tras una sesión larga. El TTL de 5 minutos significa que una pestaña abierta todo el día muestra los tipos de la última hora. Refresca con visibilitychange:
document.addEventListener('visibilitychange', () => {
if (!document.hidden) fetchRates(from.value)
})Deriva de coma flotante. Los números en JavaScript son doubles. 0.1 + 0.2 !== 0.3. Para cantidades monetarias, multiplica a céntimos enteros (amount * 100), haz la operación, después divide. O usa una librería como dinero.js para cualquier cosa relacionada con checkout.
Errores CORS en desarrollo. Algunas APIs de divisas no permiten llamadas directas desde el navegador. Finexly permite orígenes de navegador para uso del lado del cliente; el proxy del Paso 7 soluciona el resto.
Por qué Finexly para proyectos Vue
Algunas cosas importan al elegir una API de tipos de cambio para una aplicación frontend: tiempo de respuesta, precisión, generosidad del plan gratuito y JSON limpio. Finexly apunta a las cuatro — latencia p95 sub-50ms, tipos mid-market actualizados cada 60 segundos, 170+ divisas y una forma de JSON que cae directamente en una ref de Vue sin manipulación.
Si quieres ver cómo se compara con alternativas, nuestra comparación de APIs de divisas repasa los compromisos en detalle. O usa la página de comparación lado a lado.
Preguntas frecuentes
¿Puedo usar este tutorial con Vue 2?
Sí. El patrón composable es solo Vue 3, pero la lógica subyacente — pedir tipos, guardarlos en data, calcular la conversión — funciona idéntica en Options API. El ejemplo Vue 2 de arriba es un reemplazo directo.
¿La API de Finexly es gratuita para proyectos Vue?
El plan gratuito te da 1.000 peticiones al mes, más que suficiente para un proyecto personal, una pieza de portfolio o un SaaS pequeño. Con caché de 5 minutos eso soporta unos 200 usuarios activos diarios. Consulta planes de precios para volúmenes mayores.
¿Cómo evito exponer mi clave API en el bundle del cliente?
Redirige la petición a través de una ruta de servidor como muestra el Paso 7. Los prefijos VITE_* y NUXT_PUBLIC_* hacen las variables visibles al cliente. Cualquier cosa sensible debe vivir tras una función de servidor.
¿Qué tan precisos son los tipos?
Finexly agrega tipos mid-market de múltiples proveedores de liquidez Tier-1 y los refresca cada 60 segundos. Es lo bastante preciso para visualización y la mayoría de aplicaciones de pricing. Para ejecución de operaciones, querrías un feed en streaming — consulta nuestra guía REST vs WebSocket.
¿Puedo convertir cantidades históricas?
Sí — el endpoint /historical de Finexly acepta un parámetro de fecha y devuelve tipos de cualquier día hábil. El patrón es idéntico al endpoint /latest de arriba; solo cambia la URL. La guía de la API de tipos históricos lo cubre en detalle.
Resumen
Ahora tienes un conversor de divisas Vue 3 que maneja preocupaciones del mundo real — caché, debouncing, estados de error, SSR y seguridad de la clave API. El patrón composable significa que puedes integrar el mismo useCurrencyRates en un widget de la barra de navegación, una página de checkout o un gráfico del dashboard sin reescribir la lógica de fetch.
¿Listo para probarlo en tu propio proyecto? Consigue tu clave API gratuita de Finexly — sin tarjeta de crédito. Empieza con 1.000 peticiones gratuitas al mes y mejora cuando tu tráfico supere el plan gratuito.
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 →