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 includes 300 requests per month. 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's free tier includes 300 requests per month. With a 5-minute cache, even a high-traffic page would only make ~288 requests per day 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 of 300 requests per month.
- 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 includes 300 requests per month, which is more than sufficient when combined with the client-side caching strategy shown above. A 5-minute cache TTL means you will make at most 288 requests per day 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