Guide

Fetch exchange rates in JavaScript (fetch API, retries, caching)

From a bare fetch to something you would actually ship.

Intermediate9 min read

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.