Home Documentation 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 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">
      &#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'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

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