Skip to content

Fetching Exchange Rates

The app adds and deletes expenses. Now add the feature that sets BudgetBuddy apart: live exchange rates from the ExchangeRate-API, so users can see their totals in any supported currency.

Copy the final version from Module 06:

api.js
const BASE_URL = 'https://open.er-api.com/v6/latest';
const SUPPORTED_CURRENCIES = ['USD', 'EUR', 'GBP', 'CAD', 'JPY', 'AUD', 'CHF', 'MXN'];
let cachedRates = null;
let lastFetchTime = null;
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
export async function fetchRates(base = 'USD') {
const now = Date.now();
if (cachedRates && lastFetchTime && now - lastFetchTime < CACHE_TTL) {
return cachedRates;
}
const response = await fetch(`${BASE_URL}/${base}`);
if (!response.ok) throw new Error(`Exchange rate API returned ${response.status}`);
const data = await response.json();
if (data.result !== 'success') throw new Error(`API error: ${data['error-type']}`);
cachedRates = SUPPORTED_CURRENCIES.reduce((acc, code) => {
if (data.rates[code] !== undefined) acc[code] = data.rates[code];
return acc;
}, {});
lastFetchTime = now;
return cachedRates;
}
export function convertAmount(amount, from, to, rates) {
if (from === to) return amount;
const fromRate = rates[from];
const toRate = rates[to];
if (!fromRate || !toRate) throw new Error(`Unknown currency: ${from} or ${to}`);
return (amount / fromRate) * toRate;
}

Update main.js to fetch rates asynchronously during initialization and render with the real rates:

// main.js — updated init section
async function init() {
showRatesLoading(true);
try {
rates = await fetchRates();
} catch (error) {
console.warn('Exchange rates unavailable, using USD:', error.message);
} finally {
showRatesLoading(false);
}
render();
}
function showRatesLoading(loading) {
const select = document.querySelector('#currency-select');
select.disabled = loading;
}
init();

Remove the standalone render() call at the bottom of main.jsinit() calls it after rates load.

Wire the currency selector to update the display when the user picks a different currency:

// main.js (addition)
document.querySelector('#currency-select').addEventListener('change', async event => {
currentCurrency = event.target.value;
render();
});

render() calls updateTotal(list, rates, currentCurrency) — since rates contains all supported currencies pre-fetched at startup, switching currencies is instant. No additional network request needed.

Make sure updateTotal in ui.js converts from USD correctly:

// ui.js — updateTotal (final version)
export function updateTotal(expenses, rates = { USD: 1 }, currency = 'USD') {
const totalUSD = expenses.reduce((acc, e) => acc + e.amount, 0);
const rate = rates[currency] ?? 1;
const converted = totalUSD * rate;
const formatted = converted.toLocaleString('en-US', {
style: 'currency',
currency,
minimumFractionDigits: currency === 'JPY' ? 0 : 2,
maximumFractionDigits: currency === 'JPY' ? 0 : 2,
});
document.querySelector('#total-amount').textContent = formatted;
document.querySelector('#total-label').textContent = `Total (${currency}):`;
}

JPY has no decimal places — minimumFractionDigits: 0 handles that correctly.

While you are wiring handlers, add the category filter:

// main.js (addition)
document.querySelector('#category-filter').addEventListener('change', event => {
currentCategory = event.target.value;
render();
});

This updates currentCategory and calls render()getDisplayList() already uses currentCategory to filter.

With all handlers wired:

  1. Open the app in a local server
  2. Add several expenses across different categories
  3. Change the display currency — the total should update instantly
  4. Filter by category — the list should narrow and the total should update to reflect only the visible expenses
  5. Open DevTools → Network — confirm the exchange rate request fires once and is cached on subsequent currency changes
  1. Implement api.js as shown above.
  2. Update main.js with the async init() function.
  3. Add the currency change handler.
  4. Add the category filter handler.
  5. Verify all four features work together: add, delete, filter, currency conversion.
  6. Confirm the API call fires only once (check the Network tab).
  • api.js fetches all supported currencies at once and caches the result for 1 hour.
  • Startup fetches rates asynchronously — the UI renders with a USD fallback if the request fails.
  • Switching currencies uses the already-cached rates — no additional network requests.
  • updateTotal converts USD amounts using the rate for the selected currency.
  • Intl.NumberFormat (via toLocaleString) handles currency formatting including JPY zero-decimal.