Back to Documentation

Next.js Integration Guide

Learn how to integrate FXRateSync API with Next.js 14 using App Router, Server Components, API Routes, and SSR patterns.

What You'll Learn

  • • Build currency apps with Next.js 14 App Router
  • • Create API routes for secure server-side calls
  • • Implement SSR and SSG for better performance
  • • Use Server Components and Client Components effectively
  • • Handle caching and revalidation strategies

Prerequisites

This guide assumes you have:

  • Next.js 14+ with App Router
  • Basic knowledge of React and TypeScript
  • FXRateSync API key

Step 1: Environment Setup

First, configure your environment variables:

.env.local
# FXRateSync API Configuration
FXRATESYNC_API_URL=https://api.fxratesync.io
FXRATESYNC_API_KEY=your_api_key_here

# Next.js Public Variables (for client-side)
NEXT_PUBLIC_API_URL=https://api.fxratesync.io

Step 2: Create API Client Utility

Create a utility for handling API calls:

lib/fxratesync.ts
export interface ConversionResponse {
  from: string;
  to: string;
  amount: number;
  converted_amount: number;
  rate: number;
  timestamp: string;
  source: string;
}

export interface CurrencyRate {
  code: string;
  rate: number;
  name: string;
}

export interface HistoricalRate {
  date: string;
  rates: Record<string, number>;
}

export class FXRateSyncClient {
  private apiUrl: string;
  private apiKey: string;

  constructor(apiUrl: string, apiKey: string) {
    this.apiUrl = apiUrl;
    this.apiKey = apiKey;
  }

  private async request(endpoint: string, options: RequestInit = {}): Promise<any> {
    const url = `${this.apiUrl}${endpoint}`;
    
    const response = await fetch(url, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': this.apiKey,
        ...options.headers,
      },
    });

    if (!response.ok) {
      throw new Error(`API Error: ${response.status} ${response.statusText}`);
    }

    return response.json();
  }

  async convert(from: string, to: string, amount: number): Promise<ConversionResponse> {
    const params = new URLSearchParams({
      from,
      to,
      amount: amount.toString(),
    });

    return this.request(`/api/v1/convert?${params}`);
  }

  async getLatestRates(base: string = 'USD'): Promise<{ rates: Record<string, number> }> {
    const params = new URLSearchParams({ base });
    return this.request(`/api/v1/latest?${params}`);
  }

  async getHistoricalRates(date: string, base: string = 'USD'): Promise<HistoricalRate> {
    const params = new URLSearchParams({ date, base });
    return this.request(`/api/v1/historical?${params}`);
  }

  async getSupportedCurrencies(): Promise<{ currencies: Array<{ code: string; name: string }> }> {
    return this.request('/api/v1/currencies');
  }
}

// Server-side client (uses secret API key)
export const fxratesync = new FXRateSyncClient(
  process.env.FXRATESYNC_API_URL || 'https://api.fxratesync.io',
  process.env.FXRATESYNC_API_KEY || ''
);

// Client-side helper (for public endpoints only)
export const createClientSideFXRateSync = (apiKey?: string) => {
  return new FXRateSyncClient(
    process.env.NEXT_PUBLIC_API_URL || 'https://api.fxratesync.io',
    apiKey || ''
  );
};

Step 3: Create API Routes

Create secure API routes for server-side operations:

app/api/convert/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { fxratesync } from '@/lib/fxratesync';

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const from = searchParams.get('from');
    const to = searchParams.get('to');
    const amount = searchParams.get('amount');

    if (!from || !to || !amount) {
      return NextResponse.json(
        { error: 'Missing required parameters: from, to, amount' },
        { status: 400 }
      );
    }

    const amountNumber = parseFloat(amount);
    if (isNaN(amountNumber) || amountNumber <= 0) {
      return NextResponse.json(
        { error: 'Amount must be a positive number' },
        { status: 400 }
      );
    }

    const result = await fxratesync.convert(from, to, amountNumber);
    
    // Add caching headers
    const response = NextResponse.json(result);
    response.headers.set('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600');
    
    return response;
  } catch (error) {
    console.error('Currency conversion error:', error);
    return NextResponse.json(
      { error: 'Failed to convert currency' },
      { status: 500 }
    );
  }
}
app/api/rates/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { fxratesync } from '@/lib/fxratesync';

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const base = searchParams.get('base') || 'USD';

    const result = await fxratesync.getLatestRates(base);
    
    // Add caching headers for rates (cache for 5 minutes)
    const response = NextResponse.json(result);
    response.headers.set('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600');
    
    return response;
  } catch (error) {
    console.error('Rates fetch error:', error);
    return NextResponse.json(
      { error: 'Failed to fetch rates' },
      { status: 500 }
    );
  }
}

Step 4: Server Component with SSR

Create a server component that fetches data at build time:

app/rates/page.tsx
import { fxratesync } from '@/lib/fxratesync';
import { Suspense } from 'react';

// This is a Server Component - it runs on the server
export default async function RatesPage() {
  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-6">Live Exchange Rates</h1>
      
      <div className="grid md:grid-cols-2 gap-6">
        <Suspense fallback={<RatesSkeleton />}>
          <USDRates />
        </Suspense>
        
        <Suspense fallback={<RatesSkeleton />}>
          <EURRates />
        </Suspense>
      </div>
    </div>
  );
}

// Server Component for USD rates
async function USDRates() {
  try {
    const { rates } = await fxratesync.getLatestRates('USD');
    
    return (
      <div className="bg-background rounded-lg shadow-md p-6">
        <h2 className="text-xl font-semibold mb-4">USD Base Rates</h2>
        <div className="space-y-2">
          {Object.entries(rates).slice(0, 10).map(([currency, rate]) => (
            <div key={currency} className="flex justify-between items-center py-2 border-b">
              <span className="font-medium">{currency}</span>
              <span className="text-lg">{rate.toFixed(4)}</span>
            </div>
          ))}
        </div>
      </div>
    );
  } catch (error) {
    return (
      <div className="bg-destructive/10 border border-destructive/30 rounded-lg p-6">
        <p className="text-destructive-foreground">Failed to load USD rates</p>
      </div>
    );
  }
}

// Server Component for EUR rates
async function EURRates() {
  try {
    const { rates } = await fxratesync.getLatestRates('EUR');
    
    return (
      <div className="bg-background rounded-lg shadow-md p-6">
        <h2 className="text-xl font-semibold mb-4">EUR Base Rates</h2>
        <div className="space-y-2">
          {Object.entries(rates).slice(0, 10).map(([currency, rate]) => (
            <div key={currency} className="flex justify-between items-center py-2 border-b">
              <span className="font-medium">{currency}</span>
              <span className="text-lg">{rate.toFixed(4)}</span>
            </div>
          ))}
        </div>
      </div>
    );
  } catch (error) {
    return (
      <div className="bg-destructive/10 border border-destructive/30 rounded-lg p-6">
        <p className="text-destructive-foreground">Failed to load EUR rates</p>
      </div>
    );
  }
}

// Loading skeleton component
function RatesSkeleton() {
  return (
    <div className="bg-background rounded-lg shadow-md p-6 animate-pulse">
      <div className="h-6 bg-muted rounded mb-4"></div>
      <div className="space-y-2">
        {[...Array(10)].map((_, i) => (
          <div key={i} className="flex justify-between items-center py-2">
            <div className="h-4 bg-muted rounded w-16"></div>
            <div className="h-4 bg-muted rounded w-20"></div>
          </div>
        ))}
      </div>
    </div>
  );
}

// Metadata for SEO
export const metadata = {
  title: 'Live Exchange Rates - FXRateSync',
  description: 'Real-time currency exchange rates for major currencies',
  openGraph: {
    title: 'Live Exchange Rates',
    description: 'Real-time currency exchange rates for major currencies',
  },
};

Step 5: Client Component with Interactivity

Create a client component for interactive currency conversion:

components/currency-converter.tsx
'use client';

import { useState, useEffect } from 'react';
import { ConversionResponse } from '@/lib/fxratesync';

interface CurrencyConverterProps {
  initialFrom?: string;
  initialTo?: string;
  initialAmount?: number;
}

export default function CurrencyConverter({ 
  initialFrom = 'USD', 
  initialTo = 'EUR', 
  initialAmount = 100 
}: CurrencyConverterProps) {
  const [from, setFrom] = useState(initialFrom);
  const [to, setTo] = useState(initialTo);
  const [amount, setAmount] = useState(initialAmount.toString());
  const [result, setResult] = useState<ConversionResponse | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [currencies, setCurrencies] = useState<string[]>([]);

  // Load supported currencies on mount
  useEffect(() => {
    const loadCurrencies = async () => {
      try {
        const response = await fetch('/api/currencies');
        const data = await response.json();
        setCurrencies(data.currencies.map((c: any) => c.code));
      } catch (error) {
        console.error('Failed to load currencies:', error);
        // Fallback currencies
        setCurrencies(['USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'CHF', 'CNY']);
      }
    };
    loadCurrencies();
  }, []);

  const handleConvert = async () => {
    if (!amount || isNaN(parseFloat(amount))) {
      setError('Please enter a valid amount');
      return;
    }

    setLoading(true);
    setError(null);

    try {
      const response = await fetch(
        `/api/convert?from=${from}&to=${to}&amount=${amount}`
      );
      
      if (!response.ok) {
        throw new Error('Conversion failed');
      }

      const data = await response.json();
      setResult(data);
    } catch (error) {
      setError('Failed to convert currency. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  const swapCurrencies = () => {
    setFrom(to);
    setTo(from);
  };

  const formatCurrency = (amount: number, currency: string) => {
    try {
      return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency,
        minimumFractionDigits: 2,
        maximumFractionDigits: 6
      }).format(amount);
    } catch (error) {
      return `${amount.toLocaleString()} ${currency}`;
    }
  };

  return (
    <div className="max-w-md mx-auto bg-background rounded-lg shadow-md p-6">
      <h2 className="text-2xl font-bold text-center mb-6">Currency Converter</h2>
      
      <div className="space-y-4">
        {/* Amount Input */}
        <div>
          <label className="block text-sm font-medium text-foreground mb-1">
            Amount
          </label>
          <input
            type="number"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
            className="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            placeholder="Enter amount"
            step="0.01"
            min="0.01"
          />
        </div>

        {/* From Currency */}
        <div>
          <label className="block text-sm font-medium text-foreground mb-1">
            From
          </label>
          <select
            value={from}
            onChange={(e) => setFrom(e.target.value)}
            className="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
          >
            {currencies.map((currency) => (
              <option key={currency} value={currency}>
                {currency}
              </option>
            ))}
          </select>
        </div>

        {/* Swap Button */}
        <div className="flex justify-center">
          <button
            onClick={swapCurrencies}
            className="px-4 py-2 bg-muted hover:bg-muted rounded-md transition-colors"
            type="button"
          >
            ⇄ Swap
          </button>
        </div>

        {/* To Currency */}
        <div>
          <label className="block text-sm font-medium text-foreground mb-1">
            To
          </label>
          <select
            value={to}
            onChange={(e) => setTo(e.target.value)}
            className="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
          >
            {currencies.map((currency) => (
              <option key={currency} value={currency}>
                {currency}
              </option>
            ))}
          </select>
        </div>

        {/* Convert Button */}
        <button
          onClick={handleConvert}
          disabled={loading}
          className="w-full px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:bg-muted transition-colors"
        >
          {loading ? 'Converting...' : 'Convert'}
        </button>

        {/* Error Display */}
        {error && (
          <div className="p-3 bg-destructive/10 border border-destructive/30 rounded-md">
            <p className="text-destructive-foreground text-sm">{error}</p>
          </div>
        )}

        {/* Result Display */}
        {result && !loading && (
          <div className="p-4 bg-success/10 border border-success/30 rounded-md">
            <h3 className="font-semibold text-success-foreground mb-2">
              Conversion Result
            </h3>
            <div className="text-2xl font-bold text-success-foreground mb-2">
              {formatCurrency(result.converted_amount, result.to)}
            </div>
            <div className="text-sm text-success space-y-1">
              <p>
                {formatCurrency(result.amount, result.from)} = {formatCurrency(result.converted_amount, result.to)}
              </p>
              <p>Exchange Rate: 1 {result.from} = {result.rate} {result.to}</p>
              <p>Last Updated: {new Date(result.timestamp).toLocaleString()}</p>
              <p>Source: {result.source}</p>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

Step 6: Static Generation with ISR

Create a page that uses Incremental Static Regeneration:

app/historical/[date]/page.tsx
import { fxratesync } from '@/lib/fxratesync';
import { notFound } from 'next/navigation';

interface HistoricalPageProps {
  params: {
    date: string;
  };
}

export default async function HistoricalPage({ params }: HistoricalPageProps) {
  const { date } = params;
  
  // Validate date format (YYYY-MM-DD)
  const dateRegex = /^d{4}-d{2}-d{2}$/;
  if (!dateRegex.test(date)) {
    notFound();
  }

  let historicalData;
  try {
    historicalData = await fxratesync.getHistoricalRates(date);
  } catch (error) {
    console.error('Failed to fetch historical data:', error);
    notFound();
  }

  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-6">
        Historical Rates for {new Date(date).toLocaleDateString()}
      </h1>
      
      <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
        {Object.entries(historicalData.rates).map(([currency, rate]) => (
          <div key={currency} className="bg-background rounded-lg shadow-md p-4">
            <div className="flex justify-between items-center">
              <span className="font-medium text-lg">{currency}</span>
              <span className="text-xl font-bold text-primary">
                {rate.toFixed(4)}
              </span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// Generate static params for popular dates
export async function generateStaticParams() {
  const dates = [];
  const today = new Date();
  
  // Generate params for last 30 days
  for (let i = 0; i < 30; i++) {
    const date = new Date(today);
    date.setDate(date.getDate() - i);
    dates.push({
      date: date.toISOString().split('T')[0]
    });
  }
  
  return dates;
}

// Enable ISR with 1 hour revalidation
export const revalidate = 3600;

// Metadata
export async function generateMetadata({ params }: HistoricalPageProps) {
  const { date } = params;
  const formattedDate = new Date(date).toLocaleDateString();
  
  return {
    title: `Historical Rates for ${formattedDate} - FXRateSync`,
    description: `View historical exchange rates for ${formattedDate}`,
  };
}

Step 7: Custom Hook for Client-Side Data

Create a custom hook for managing currency data:

hooks/use-currency-data.ts
'use client';

import { useState, useEffect, useCallback } from 'react';
import { ConversionResponse } from '@/lib/fxratesync';

interface UseCurrencyDataReturn {
  convert: (from: string, to: string, amount: number) => Promise<void>;
  result: ConversionResponse | null;
  loading: boolean;
  error: string | null;
  clearError: () => void;
}

export function useCurrencyData(): UseCurrencyDataReturn {
  const [result, setResult] = useState<ConversionResponse | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const convert = useCallback(async (from: string, to: string, amount: number) => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(
        `/api/convert?from=${from}&to=${to}&amount=${amount}`
      );
      
      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error || 'Conversion failed');
      }

      const data = await response.json();
      setResult(data);
    } catch (error) {
      setError(error instanceof Error ? error.message : 'An error occurred');
      setResult(null);
    } finally {
      setLoading(false);
    }
  }, []);

  const clearError = useCallback(() => {
    setError(null);
  }, []);

  return {
    convert,
    result,
    loading,
    error,
    clearError,
  };
}

// Hook for fetching rates
export function useRates(baseCurrency: string = 'USD') {
  const [rates, setRates] = useState<Record<string, number>>({});
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchRates = async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(`/api/rates?base=${baseCurrency}`);
        if (!response.ok) {
          throw new Error('Failed to fetch rates');
        }

        const data = await response.json();
        setRates(data.rates);
      } catch (error) {
        setError(error instanceof Error ? error.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };

    fetchRates();
  }, [baseCurrency]);

  return { rates, loading, error };
}

Best Practices

  • Server Components: Use for data fetching and SEO-critical content
  • Client Components: Use for interactivity and user input
  • API Routes: Keep API keys secure on the server
  • Caching: Use appropriate cache headers and ISR
  • Error Boundaries: Implement proper error handling
  • TypeScript: Use proper typing for better DX

Performance Optimization

Configure Next.js for optimal performance:

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Enable static exports for better performance
  output: 'standalone',
  
  // Optimize images
  images: {
    domains: ['api.fxratesync.io'],
    formats: ['image/webp', 'image/avif'],
  },
  
  // Enable compression
  compress: true,
  
  // Configure caching
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=300, stale-while-revalidate=600',
          },
        ],
      },
    ];
  },
  
  // Enable experimental features
  experimental: {
    typedRoutes: true,
  },
};

module.exports = nextConfig;

Complete Next.js Example

Ready to build a full-featured currency app with Next.js?