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.
| Source | Good for | Historical data | Notes |
|---|---|---|---|
| Frankfurter | Tutorials and major-pair prototypes | Yes | Simple ECB-backed reference data. |
| European Central Bank | Official EUR reference rates | Yes | Useful for source attribution. |
| Bank of Canada | CAD reference rates | Yes | Official public source. |
| FXPeek | Converter UX, historical pages, CSV/API options | Yes | My 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.