Guide
Fetch exchange rates in JavaScript (fetch API, retries, caching)
From a bare fetch to something you would actually ship.
What this covers
The naive version of this integration is one fetch in a page component. It works, and then it fails in three predictable ways: the key ends up in the bundle, the call is repeated on every render, and the day a 503 arrives the page renders an empty price.
This guide builds the version you would keep: the key stays on the server, the reading is cached for a sensible window, failures are typed, and conversion is arithmetic instead of another request.
Call it from the server, not the browser
A bearer key in client-side JavaScript is public. Make the call in a route handler or server function and pass the display data down. If you are on a framework without a server, use the open endpoint from the browser and accept its windows.
The response carries the reading timestamp in meta.updated_at — keep it, because it is the honest 'as of' line for the figure you render.
Node / edge handler
export async function GET() {
const res = await fetch("https://fiatagent.base44.app/api/v1/rates", {
headers: { Authorization: "Bearer " + process.env.FIAT_AGENT_KEY },
});
if (res.status === 429) {
// A keyed 429 is a spent per-key burst window: back off using retry-after.
return Response.json({ error: "unexpected_rate_limit" }, { status: 502 });
}
if (!res.ok) {
return Response.json({ error: "upstream_" + res.status }, { status: 502 });
}
const { meta, data } = await res.json();
return Response.json(
{ rates: data.latest.rates, asOf: meta.updated_at, base: meta.base_currency },
{ headers: { "cache-control": "public, s-maxage=900, stale-while-revalidate=3600" } },
);
}Retry only what should be retried
A 503 service_busy means the store could not be read in time and is worth one retry after the retry-after header. A 401 will never succeed on retry — a bad key stays a bad key. Treating those the same is how a small upstream blip becomes a request storm.
JavaScript · retry with the server's own backoff
async function readRates(attempt = 0) {
const res = await fetch("https://fiatagent.base44.app/api/v1/rates", {
headers: { Authorization: "Bearer " + process.env.FIAT_AGENT_KEY },
});
if (res.status === 503 && attempt < 2) {
const wait = Number(res.headers.get("retry-after") ?? 2) * 1000;
await new Promise((resolve) => setTimeout(resolve, wait));
return readRates(attempt + 1);
}
if (!res.ok) throw new Error("Fiat Agent: " + res.status);
return res.json();
}Cache the reading, not the request
Rates move on the store's cadence, not on your traffic. Caching one reading for 15 minutes means a thousand page views cost one request, and the page can still state exactly how old its figure is.
In a single process a module-level cache is enough. In a serverless runtime, module state is reset freely — use the platform cache or a CDN header instead of assuming it survives.
JavaScript · a 15-minute reading cache
let cached = { at: 0, payload: null };
const TTL = 15 * 60 * 1000;
async function getRates() {
if (cached.payload && Date.now() - cached.at < TTL) return cached.payload;
const payload = await readRates();
cached = { at: Date.now(), payload };
return payload;
}Convert amounts without another call
Every quote in a response shares one base, so a cross rate is a division of two published figures. Round for display, never for storage — rounding the rate before multiplying is the most common source of a price that is a cent off.
JavaScript · display-safe conversion
const format = (amount, currency, rate, locale = "en-US") =>
new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount * rate);
const { rates } = await getRates();
const gbpPerEur = rates.GBP / rates.EUR;
console.log(format(49.99, "GBP", gbpPerEur)); // £43.12 (example)Common questions
Should the API key live in my frontend?
No. A bearer key in client-side code is readable by anyone. Call the API from your server and pass down only the rates you display.
How often should I refresh?
No more often than you would act on a change. Fifteen minutes suits most price displays; the reading timestamp in the response means you can always show how current the figure is.
Why did I get a 401 when my key works elsewhere?
The header must be exactly Authorization: Bearer <key>. A missing Bearer prefix, a trailing newline from an environment file, or a revoked key all answer 401 — the error code in the body says which.
Run it against the real thing.
The open endpoint needs no key, so you can paste these samples into a terminal before you sign up for anything.