Build a React Currency Converter with FxFeed API
Learn how to build a real-time currency converter in React using the FxFeed API. Complete tutorial with code examples, error handling, and best practices.
Building a currency converter is one of the most practical ways to learn API integration in React. In this tutorial, you'll create a fully functional currency converter that fetches real-time exchange rates from the FxFeed API.
What You'll Build
By the end of this tutorial, you'll have:
- A React component that converts between 160+ currencies
- Real-time exchange rate fetching
- Clean error handling and loading states
- TypeScript types for type safety
Prerequisites
- Basic React knowledge (hooks, state)
- Node.js installed
- A free FxFeed API key (get one at fxfeed.io)
Step 1: Set Up Your Project
Create a new React project with TypeScript:
npx create-react-app currency-converter --template typescript
cd currency-converter
Or if you're using Vite:
npm create vite@latest currency-converter -- --template react-ts
cd currency-converter
npm install
Step 2: Get Your API Key
- Sign up at FxFeed.io
- Navigate to your dashboard
- Copy your API key
Create a .env file in your project root:
REACT_APP_FXFEED_API_KEY=your_api_key_here
For Vite, use VITE_FXFEED_API_KEY instead.
Step 3: Define TypeScript Types
Create a new file src/types/currency.ts:
export interface ExchangeRateResponse {
success: boolean;
timestamp: number;
base: string;
date: string;
rates: Record<string, number>;
}
export interface ConvertResponse {
success: boolean;
query: {
from: string;
to: string;
amount: number;
};
result: number;
rate: number;
}
export interface Currency {
code: string;
name: string;
symbol: string;
}
Step 4: Create the API Service
Create src/services/fxfeed.ts:
const API_BASE = 'https://api.fxfeed.io/v2';
const API_KEY = process.env.REACT_APP_FXFEED_API_KEY;
export async function getLatestRates(
base: string,
currencies?: string[]
): Promise<ExchangeRateResponse> {
const params = new URLSearchParams({
api_key: API_KEY!,
base,
});
if (currencies?.length) {
params.append('currencies', currencies.join(','));
}
const response = await fetch(`${API_BASE}/latest?${params}`);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
export async function convertCurrency(
from: string,
to: string,
amount: number
): Promise<ConvertResponse> {
const params = new URLSearchParams({
api_key: API_KEY!,
from,
to,
amount: amount.toString(),
});
const response = await fetch(`${API_BASE}/convert?${params}`);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
Step 5: Build the Currency Converter Component
Create src/components/CurrencyConverter.tsx:
import React, { useState, useEffect, useCallback } from 'react';
import { getLatestRates } from '../services/fxfeed';
// Popular currencies for the dropdown
const CURRENCIES = [
{ code: 'USD', name: 'US Dollar', symbol: '$' },
{ code: 'EUR', name: 'Euro', symbol: '€' },
{ code: 'GBP', name: 'British Pound', symbol: '£' },
{ code: 'JPY', name: 'Japanese Yen', symbol: '¥' },
{ code: 'AUD', name: 'Australian Dollar', symbol: 'A$' },
{ code: 'CAD', name: 'Canadian Dollar', symbol: 'C$' },
{ code: 'CHF', name: 'Swiss Franc', symbol: 'Fr' },
{ code: 'CNY', name: 'Chinese Yuan', symbol: '¥' },
{ code: 'INR', name: 'Indian Rupee', symbol: '₹' },
{ code: 'MXN', name: 'Mexican Peso', symbol: '$' },
];
export function CurrencyConverter() {
const [amount, setAmount] = useState<number>(100);
const [fromCurrency, setFromCurrency] = useState<string>('USD');
const [toCurrency, setToCurrency] = useState<string>('EUR');
const [result, setResult] = useState<number | null>(null);
const [rate, setRate] = useState<number | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const convert = useCallback(async () => {
if (amount <= 0) return;
setLoading(true);
setError(null);
try {
const data = await getLatestRates(fromCurrency, [toCurrency]);
const exchangeRate = data.rates[toCurrency];
if (exchangeRate) {
setRate(exchangeRate);
setResult(amount * exchangeRate);
} else {
throw new Error('Rate not available');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Conversion failed');
setResult(null);
setRate(null);
} finally {
setLoading(false);
}
}, [amount, fromCurrency, toCurrency]);
// Auto-convert when inputs change
useEffect(() => {
const debounce = setTimeout(convert, 300);
return () => clearTimeout(debounce);
}, [convert]);
const swapCurrencies = () => {
setFromCurrency(toCurrency);
setToCurrency(fromCurrency);
};
const formatNumber = (num: number) => {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 4,
}).format(num);
};
return (
<div className="converter-container">
<h2>Currency Converter</h2>
<div className="input-group">
<label htmlFor="amount">Amount</label>
<input
id="amount"
type="number"
value={amount}
onChange={(e) => setAmount(parseFloat(e.target.value) || 0)}
min="0"
step="0.01"
/>
</div>
<div className="currency-selectors">
<div className="selector">
<label htmlFor="from">From</label>
<select
id="from"
value={fromCurrency}
onChange={(e) => setFromCurrency(e.target.value)}
>
{CURRENCIES.map((currency) => (
<option key={currency.code} value={currency.code}>
{currency.code} - {currency.name}
</option>
))}
</select>
</div>
<button
className="swap-button"
onClick={swapCurrencies}
aria-label="Swap currencies"
>
⇄
</button>
<div className="selector">
<label htmlFor="to">To</label>
<select
id="to"
value={toCurrency}
onChange={(e) => setToCurrency(e.target.value)}
>
{CURRENCIES.map((currency) => (
<option key={currency.code} value={currency.code}>
{currency.code} - {currency.name}
</option>
))}
</select>
</div>
</div>
<div className="result-section">
{loading && <p className="loading">Converting...</p>}
{error && <p className="error">{error}</p>}
{result !== null && !loading && !error && (
<>
<p className="result">
{formatNumber(amount)} {fromCurrency} =
<strong> {formatNumber(result)} {toCurrency}</strong>
</p>
{rate && (
<p className="rate">
1 {fromCurrency} = {formatNumber(rate)} {toCurrency}
</p>
)}
</>
)}
</div>
</div>
);
}
Step 6: Add Styling
Create src/components/CurrencyConverter.css:
.converter-container {
max-width: 500px;
margin: 2rem auto;
padding: 2rem;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.converter-container h2 {
margin-bottom: 1.5rem;
text-align: center;
color: #1a1a2e;
}
.input-group {
margin-bottom: 1.5rem;
}
.input-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #4a4a4a;
}
.input-group input {
width: 100%;
padding: 0.75rem;
font-size: 1.25rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
transition: border-color 0.2s;
}
.input-group input:focus {
outline: none;
border-color: #4f46e5;
}
.currency-selectors {
display: flex;
align-items: flex-end;
gap: 1rem;
margin-bottom: 1.5rem;
}
.selector {
flex: 1;
}
.selector label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #4a4a4a;
}
.selector select {
width: 100%;
padding: 0.75rem;
font-size: 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
background: white;
cursor: pointer;
}
.swap-button {
padding: 0.75rem 1rem;
font-size: 1.25rem;
background: #4f46e5;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
}
.swap-button:hover {
background: #4338ca;
}
.result-section {
padding: 1.5rem;
background: #f8f9fa;
border-radius: 8px;
text-align: center;
}
.loading {
color: #6b7280;
font-style: italic;
}
.error {
color: #dc2626;
}
.result {
font-size: 1.25rem;
color: #1a1a2e;
}
.result strong {
color: #4f46e5;
font-size: 1.5rem;
}
.rate {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #6b7280;
}
Step 7: Use the Component
Update your src/App.tsx:
import { CurrencyConverter } from './components/CurrencyConverter';
import './components/CurrencyConverter.css';
function App() {
return (
<div className="App">
<CurrencyConverter />
</div>
);
}
export default App;
Step 8: Add Error Boundaries and Loading States
For production apps, wrap your converter in an error boundary:
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
}
class ErrorBoundary extends Component<Props, State> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
Best Practices
1. Cache API Responses
Exchange rates don't change every second. Cache responses to reduce API calls:
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
const cache = new Map<string, { data: any; timestamp: number }>();
async function getCachedRates(base: string, currencies: string[]) {
const key = `${base}-${currencies.sort().join(',')}`;
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return cached.data;
}
const data = await getLatestRates(base, currencies);
cache.set(key, { data, timestamp: Date.now() });
return data;
}
2. Handle Rate Limits
FxFeed's free tier includes 5,000 requests/month. Implement rate limiting:
const requestTimes: number[] = [];
const MAX_REQUESTS_PER_MINUTE = 60;
async function rateLimitedFetch(url: string) {
const now = Date.now();
const oneMinuteAgo = now - 60000;
// Remove old timestamps
while (requestTimes.length && requestTimes[0] < oneMinuteAgo) {
requestTimes.shift();
}
if (requestTimes.length >= MAX_REQUESTS_PER_MINUTE) {
throw new Error('Rate limit exceeded. Please wait.');
}
requestTimes.push(now);
return fetch(url);
}
3. Debounce User Input
Avoid making API calls on every keystroke:
import { useDebouncedCallback } from 'use-debounce';
const debouncedConvert = useDebouncedCallback(convert, 300);
Next Steps
Now that you have a working currency converter, consider:
- Add more currencies - FxFeed supports 160+ currencies
- Historical rates - Show rate trends with charts
- Favorites - Let users save frequently used pairs
- Offline support - Cache recent rates for offline use
Get Started with FxFeed
Ready to build? Get your free API key and start converting currencies in minutes.
The free tier includes:
- 5,000 API requests per month
- 160+ currencies
- Daily rate updates
- No credit card required
Ready to integrate FX rates?
Start using FxFeed.io today with our free tier. No credit card required.