Back to Blog
7 min read

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.

Build a React Currency Converter with FxFeed API

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

  1. Sign up at FxFeed.io
  2. Navigate to your dashboard
  3. 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:

  1. Add more currencies - FxFeed supports 160+ currencies
  2. Historical rates - Show rate trends with charts
  3. Favorites - Let users save frequently used pairs
  4. 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

View the full API documentation →

Share this article:
All Articles

Ready to integrate FX rates?

Start using FxFeed.io today with our free tier. No credit card required.