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?