How to Build a Real-Time Currency Converter with JavaScript
Building a currency converter is one of those projects that looks simple on the surface but teaches you a surprising amount about working with APIs, handling asynchronous data, and building responsive UIs. Whether you are adding multi-currency support to an e-commerce checkout or building a standalone tool, this tutorial walks you through every step.
By the end, you will have a fully working currency converter built with vanilla JavaScript -- no frameworks, no build tools, just HTML, CSS, and the Fetch API.
What you will build: A responsive currency converter that fetches live exchange rates, supports 160+ currencies, includes client-side caching, and handles errors gracefully.
Prerequisites
Before you start, make sure you have:
- Basic knowledge of HTML, CSS, and JavaScript
- A modern browser (Chrome, Firefox, Edge, Safari)
- A free API key from AllRatesToday for live exchange rate data
- A text editor (VS Code, Sublime Text, or anything you prefer)
AllRatesToday provides real-time exchange rates for 160+ currencies, updated every 60 seconds, with a free tier that is more than enough for development and light production use.
Step 1: Setting Up the HTML Structure
Start with a clean HTML file that includes a form with two currency selectors, an amount input, and a result display area.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Currency Converter</title>
</head>
<body>
<div class="converter">
<h1>Currency Converter</h1>
<div class="field">
<label for="amount">Amount</label>
<input type="number" id="amount" value="1" min="0" step="any">
</div>
<div class="field">
<label for="from-currency">From</label>
<select id="from-currency"></select>
</div>
<button class="swap-btn" id="swap-btn" title="Swap currencies">
⇅
</button>
<div class="field">
<label for="to-currency">To</label>
<select id="to-currency"></select>
</div>
<button id="convert-btn">Convert</button>
<div class="result" id="result" style="display:none;"></div>
</div>
<script src="converter.js"></script>
</body>
</html> This gives you a clean structure with two <select> elements that will be populated dynamically with currency codes from the API. Add your own CSS to style the card layout -- or use the complete example at the end of this tutorial.
Step 2: Fetching Exchange Rates from the API
Now create converter.js. The first thing you need is a function that fetches live exchange rates. The AllRatesToday API follows a straightforward REST pattern -- send your API key and a base currency, and you get back every supported exchange rate in one response.
const API_KEY = 'YOUR_API_KEY'; // Get yours at allratestoday.com/register
const BASE_URL = 'https://api.allratestoday.com/v1';
async function fetchRates(baseCurrency) {
const response = await fetch(
`${BASE_URL}/latest?apikey=${API_KEY}&base=${baseCurrency}`
);
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.rates;
} This function takes a base currency code (like USD), calls the API, and returns an object of exchange rates. The rates object looks something like { EUR: 0.92, GBP: 0.79, JPY: 149.50, ... }.
Populating the Currency Dropdowns
You also need to populate the dropdowns with all available currencies. Here is a dynamic approach that pulls the list directly from the API response:
async function populateCurrencies() {
const rates = await fetchRates('USD');
const currencies = ['USD', ...Object.keys(rates).sort()];
const fromSelect = document.getElementById('from-currency');
const toSelect = document.getElementById('to-currency');
currencies.forEach(code => {
fromSelect.add(new Option(code, code));
toSelect.add(new Option(code, code));
});
fromSelect.value = 'USD';
toSelect.value = 'EUR';
} Tip: AllRatesToday supports 160+ currency codes. Sorting them alphabetically makes it much easier for users to find the one they need.
Step 3: Building the Conversion Logic
The conversion itself is straightforward. When you fetch rates with a specific base currency, every rate in the response is already relative to that base. So converting 100 USD to EUR is simply 100 * rates['EUR'].
async function handleConvert() {
const btn = document.getElementById('convert-btn');
const resultDiv = document.getElementById('result');
const amount = parseFloat(document.getElementById('amount').value);
const from = document.getElementById('from-currency').value;
const to = document.getElementById('to-currency').value;
if (isNaN(amount) || amount <= 0) {
showResult('Please enter a valid amount greater than zero.', true);
return;
}
btn.disabled = true;
btn.textContent = 'Converting...';
try {
const rates = await fetchRates(from);
const converted = amount * rates[to];
showResult(
`${amount.toLocaleString()} ${from} = ${converted.toLocaleString(
undefined,
{ minimumFractionDigits: 2, maximumFractionDigits: 4 }
)} ${to}`
);
} catch (error) {
showResult(error.message, true);
} finally {
btn.disabled = false;
btn.textContent = 'Convert';
}
}
function showResult(message, isError = false) {
const el = document.getElementById('result');
el.textContent = message;
el.className = isError ? 'result error' : 'result';
el.style.display = 'block';
} Notice how we disable the button during the request and restore it in the finally block. This prevents double-clicks and gives the user clear feedback that something is happening.
Step 4: Adding Error Handling and Caching
The code above works, but it calls the API on every single conversion. That wastes requests and slows things down. Let's add a caching layer and more robust error handling.
Client-Side Caching with localStorage
Exchange rates do not change every millisecond. Caching responses for a few minutes saves API calls and makes conversions feel instant.
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
function saveCache(base, rates) {
localStorage.setItem(
`rates_${base}`,
JSON.stringify({ rates, ts: Date.now() })
);
}
function loadCache(base) {
const raw = localStorage.getItem(`rates_${base}`);
if (!raw) return null;
const { rates, ts } = JSON.parse(raw);
return (Date.now() - ts < CACHE_TTL) ? rates : null;
}
async function getRates(baseCurrency) {
const cached = loadCache(baseCurrency);
if (cached) return cached;
const rates = await fetchRates(baseCurrency);
saveCache(baseCurrency, rates);
return rates;
} Important: Always validate cached data before using it. If the cache structure changes between app versions, stale entries could cause runtime errors. Consider adding a version key to your cache entries.
Retry Logic for Network Failures
Network requests can fail for many reasons -- flaky connections, temporary server issues, or rate limiting. A retry wrapper with exponential backoff handles all of these gracefully:
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (response.status === 429) {
// Rate limited - wait and retry with exponential backoff
const waitTime = 1000 * Math.pow(2, i);
await new Promise(r => setTimeout(r, waitTime));
continue;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(r => setTimeout(r, 1000));
}
}
} Rate limits: AllRatesToday has a free tier with a monthly request quota. With a 5-minute cache, even a high-traffic page makes minimal requests per base currency. For most projects, the free tier combined with caching is more than sufficient.
Step 5: Complete Working Example
Here is the full converter.js that puts everything together -- API calls, caching, error handling, and event listeners:
const API_KEY = 'YOUR_API_KEY';
const BASE_URL = 'https://api.allratestoday.com/v1';
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
// --- Caching ---
function saveCache(base, rates) {
localStorage.setItem(
`rates_${base}`,
JSON.stringify({ rates, ts: Date.now() })
);
}
function loadCache(base) {
const raw = localStorage.getItem(`rates_${base}`);
if (!raw) return null;
const { rates, ts } = JSON.parse(raw);
return (Date.now() - ts < CACHE_TTL) ? rates : null;
}
// --- API ---
async function fetchRates(base) {
const cached = loadCache(base);
if (cached) return cached;
const res = await fetch(
`${BASE_URL}/latest?apikey=${API_KEY}&base=${base}`
);
if (res.status === 429)
throw new Error('Rate limit exceeded. Try again shortly.');
if (!res.ok)
throw new Error(`API error: ${res.status}`);
const data = await res.json();
saveCache(base, data.rates);
return data.rates;
}
// --- UI ---
async function populateCurrencies() {
try {
const rates = await fetchRates('USD');
const currencies = ['USD', ...Object.keys(rates).sort()];
const fromEl = document.getElementById('from-currency');
const toEl = document.getElementById('to-currency');
currencies.forEach(c => {
fromEl.add(new Option(c, c));
toEl.add(new Option(c, c));
});
fromEl.value = 'USD';
toEl.value = 'EUR';
} catch (err) {
showResult('Failed to load currencies. Check your API key.', true);
}
}
async function handleConvert() {
const btn = document.getElementById('convert-btn');
const amount = parseFloat(document.getElementById('amount').value);
const from = document.getElementById('from-currency').value;
const to = document.getElementById('to-currency').value;
if (isNaN(amount) || amount <= 0) {
showResult('Enter a valid amount greater than zero.', true);
return;
}
btn.disabled = true;
btn.textContent = 'Converting...';
try {
const rates = await fetchRates(from);
const converted = amount * rates[to];
showResult(
`${amount.toLocaleString()} ${from} = ${converted.toLocaleString(
undefined,
{ minimumFractionDigits: 2, maximumFractionDigits: 4 }
)} ${to}`
);
} catch (error) {
showResult(error.message, true);
} finally {
btn.disabled = false;
btn.textContent = 'Convert';
}
}
function showResult(message, isError = false) {
const el = document.getElementById('result');
el.textContent = message;
el.className = isError ? 'result error' : 'result';
el.style.display = 'block';
}
// --- Event Listeners ---
document.getElementById('convert-btn')
.addEventListener('click', handleConvert);
document.getElementById('swap-btn')
.addEventListener('click', () => {
const fromEl = document.getElementById('from-currency');
const toEl = document.getElementById('to-currency');
[fromEl.value, toEl.value] = [toEl.value, fromEl.value];
});
document.getElementById('amount')
.addEventListener('keydown', e => {
if (e.key === 'Enter') handleConvert();
});
// Initialize
populateCurrencies(); That's it! Save both files in the same directory, replace YOUR_API_KEY with your actual key from AllRatesToday, and open the HTML file in a browser.
Performance Tips
Once your converter is working, here are ways to make it faster and more reliable in production:
| Technique | Impact | Difficulty |
|---|---|---|
| Client-side caching (localStorage) | Eliminates 90%+ of API calls | Easy |
| Prefetch common currency pairs | Instant conversions for top pairs | Easy |
| Debounce live-typing input | Prevents excessive API calls | Easy |
| Service Worker for offline support | Works without network connection | Medium |
| Server-side proxy with Redis cache | Protects API key, shared cache | Medium |
- Cache aggressively. Exchange rates update every 60 seconds at most. A 5-minute client-side cache eliminates redundant requests and keeps you well within AllRatesToday's free tier.
- Prefetch common pairs. If your users mostly convert between USD, EUR, and GBP, fetch those rates on page load so conversions feel instant.
- Use the base parameter wisely. Instead of fetching all rates twice (once for each direction), fetch rates for the "from" currency and multiply directly.
- Show stale data while refreshing. Display the cached result immediately and update it silently when fresh data arrives. Users prefer a fast stale answer over a slow accurate one.
- Debounce input. If you add live-as-you-type conversion, debounce the input handler so you are not firing API calls on every keystroke.
Going Further: Using the AllRatesToday npm Package
If you are building a Node.js backend or a server-rendered app, you can skip the raw fetch calls entirely and use the official AllRatesToday npm package:
npm install allratestoday import AllRatesToday from 'allratestoday';
const client = new AllRatesToday('YOUR_API_KEY');
// Get latest rates
const rates = await client.latest({ base: 'USD' });
console.log(rates.EUR); // 0.92
// Convert directly
const result = await client.convert({
from: 'USD',
to: 'EUR',
amount: 250
});
console.log(result); // 230.00 The SDK handles retries, error parsing, and response typing out of the box, making it ideal for production server-side applications.
Frequently Asked Questions
What API should I use for a JavaScript currency converter?
AllRatesToday provides a free exchange rate API with real-time rates updated every 60 seconds for 160+ currencies. It includes an official JavaScript/Node.js SDK available on npm, making integration straightforward for both browser and server-side projects.
Can I build a currency converter with vanilla JavaScript?
Yes. Using the Fetch API and any exchange rate REST API, you can build a fully functional currency converter with plain HTML, CSS, and JavaScript -- no frameworks required, as this tutorial demonstrates.
How do I handle API rate limits in a currency converter?
Cache exchange rates locally and only fetch new rates when needed. AllRatesToday's free tier is more than sufficient when combined with the client-side caching strategy shown above. A 5-minute cache TTL keeps requests minimal per base currency.
Start Building with Real-Time Rates
Get your free API key and access 160+ currencies updated every 60 seconds.
Get Your Free API Key