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?