Home Documentation Playground Pricing API Status Blog About FAQ Support

How to Build a Real-Time Currency Converter with JavaScript

Reviewed by Madhushan, Fintech Developer — April 2026

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:

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">
      &#8645;
    </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

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