FXPeek

Developer guide

Building a currency converter with free APIs

A practical implementation pattern for exchange-rate converters, historical FX pages, caching, and reference-rate disclaimers.

Disclosure

I maintain FXPeek. This guide uses FXPeek as one implementation reference alongside public data sources such as Frankfurter, the European Central Bank, and the Bank of Canada.

1. Pick the right data source

For prototypes, Frankfurter is a practical starting point because it is simple and includes historical reference rates for many major pairs. For production, compare coverage, licensing, update frequency, uptime, historical depth, and whether the provider supports the currencies your users actually search for.

SourceGood forHistorical dataNotes
FrankfurterTutorials and major-pair prototypesYesSimple ECB-backed reference data.
European Central BankOfficial EUR reference ratesYesUseful for source attribution.
Bank of CanadaCAD reference ratesYesOfficial public source.
FXPeekConverter UX, historical pages, CSV/API optionsYesMy project; reference rates, not transaction quotes.

2. Normalize currency codes

Validate currency codes at the boundary. Do not let arbitrary strings move through your provider adapter.

const CURRENCIES = ["USD", "EUR", "GBP", "JPY", "CNY"] as const;
type Currency = typeof CURRENCIES[number];

function normalizeCurrency(value: string): Currency {
  const code = value.trim().toUpperCase();
  if (!CURRENCIES.includes(code as Currency)) {
    throw new Error(`Unsupported currency: ${value}`);
  }
  return code as Currency;
}

3. Separate the provider adapter

Your UI should not know whether a rate came from Frankfurter, a central bank file, a paid API, or your own database.

type FxRate = {
  base: Currency;
  quote: Currency;
  date: string;
  rate: number;
  source: string;
};

type RateProvider = {
  latest(base: Currency, quote: Currency): Promise<FxRate>;
  historical(base: Currency, quote: Currency, date: string): Promise<FxRate>;
};

4. Cache by pair and date

Historical daily rates should be cached by base, quote, and date. A rate for a past date rarely needs a fresh provider call.

async function getHistoricalRate(
  provider: RateProvider,
  base: Currency,
  quote: Currency,
  date: string
): Promise<FxRate> {
  const key = `fx:${base}:${quote}:${date}`;
  const cached = await cache.get<FxRate>(key);
  if (cached) return cached;

  const row = await provider.historical(base, quote, date);
  await cache.set(key, row, { ttlSeconds: 60 * 60 * 24 * 30 });
  return row;
}

5. Build pages users actually need

Strong exchange-rate pages should answer the next question, not just the first one. Include a current-rate hero, two-way converter, common amount rows, 1M/3M/1Y/5Y/MAX charts, 52-week high and low, related pairs, FAQ schema, and a clear reference-rate disclaimer.

Need historical FX data for an app?

Start with a small CSV export or lightweight API access for the pairs and date ranges your product needs. FXPeek is best suited for small finance tools, reports, prototypes, and niche currency-pair coverage.