Why Next.js Is the Right Framework for E-commerce Data Apps
Next.js 16 with the App Router is an excellent choice for building e-commerce data applications. Its Server Components, API Routes, and built-in ISR (Incremental Static Regeneration) give you:
- Server-side data fetching without exposing your API keys to the client
- Caching to reduce API calls and improve performance
- Static page generation for SEO-critical product pages
- Edge runtime support for ultra-low-latency responses globally
This guide walks through a complete integration of the Pricium product data API into a Next.js 16 application.
Project Setup
npx create-next-app@latest ecommerce-app --typescript --app --src-dir
cd ecommerce-app
npm install axios zod
Add your API key to .env.local:
PRICIUM_API_KEY=pk_live_...
Step 1: Create a Type-Safe API Client
Create src/lib/pricium.ts:
import { z } from 'zod';
const VariationSchema = z.object({
size: z.string().optional(),
color: z.string().optional(),
config: z.string().optional(),
price: z.number(),
currency: z.string(),
available: z.boolean(),
rating: z.number().optional(),
review_count: z.number().optional(),
});
const ProductDataSchema = z.object({
product_title: z.string(),
source_url: z.string(),
variations: z.array(VariationSchema),
geo_pricing: z.record(z.object({
price: z.number(),
currency: z.string(),
})).optional(),
scraped_at: z.string(),
});
export type ProductData = z.infer<typeof ProductDataSchema>;
export type Variation = z.infer<typeof VariationSchema>;
export async function fetchProductData(
url: string,
location: string = 'US'
): Promise<ProductData> {
const response = await fetch('https://api.pricium.store/product-detail', {
method: 'GET',
headers: {
'Authorization': `Bearer ${process.env.PRICIUM_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, location }),
// Next.js 16 cache control
next: { revalidate: 300 }, // Cache for 5 minutes (ISR)
});
if (!response.ok) {
throw new Error(`Pricium API error: ${response.status}`);
}
const data = await response.json();
return ProductDataSchema.parse(data);
}
Step 2: API Route for Client-Side Fetching
Create src/app/api/product/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { fetchProductData } from '@/lib/pricium';
export async function POST(req: NextRequest) {
try {
const { url, location } = await req.json();
if (!url || typeof url !== 'string') {
return NextResponse.json({ error: 'Valid URL is required' }, { status: 400 });
}
const data = await fetchProductData(url, location || 'US');
return NextResponse.json(data);
} catch (error) {
console.error('Product fetch error:', error);
return NextResponse.json({ error: 'Failed to fetch product data' }, { status: 500 });
}
}
Step 3: Server Component - Static Product Page
Create src/app/product/page.tsx for a Server Component that renders with ISR:
import { fetchProductData } from '@/lib/pricium';
import { VariationsTable } from '@/components/VariationsTable';
interface Props {
searchParams: { url?: string; location?: string };
}
export default async function ProductPage({ searchParams }: Props) {
const { url, location = 'US' } = searchParams;
if (!url) {
return <div className="p-8 text-center">Paste a product URL to get started.</div>;
}
const product = await fetchProductData(url, location);
return (
<main className="max-w-5xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-2">{product.product_title}</h1>
<p className="text-gray-500 mb-6 text-sm">
Data as of {new Date(product.scraped_at).toLocaleString()} ·{' '}
<a href={product.source_url} target="_blank" rel="noopener" className="underline">
View on source
</a>
</p>
<VariationsTable variations={product.variations} />
</main>
);
}
Step 4: Variations Table Component
Create src/components/VariationsTable.tsx:
import { Variation } from '@/lib/pricium';
interface Props {
variations: Variation[];
}
export function VariationsTable({ variations }: Props) {
const sorted = [...variations].sort((a, b) => a.price - b.price);
return (
<div className="overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-50 text-left">
<th className="p-3 font-semibold border-b">Size</th>
<th className="p-3 font-semibold border-b">Color</th>
<th className="p-3 font-semibold border-b text-right">Price</th>
<th className="p-3 font-semibold border-b text-center">Stock</th>
<th className="p-3 font-semibold border-b text-center">Rating</th>
</tr>
</thead>
<tbody>
{sorted.map((v, i) => (
<tr
key={i}
className={`border-b transition-colors ${
!v.available ? 'opacity-40' : 'hover:bg-blue-50'
}`}
>
<td className="p-3">{v.size || '-'}</td>
<td className="p-3">{v.color || '-'}</td>
<td className="p-3 text-right font-medium">
{v.currency} {v.price.toFixed(2)}
</td>
<td className="p-3 text-center">
{v.available ? (
<span className="text-green-600 font-medium">In Stock</span>
) : (
<span className="text-red-500">Out of Stock</span>
)}
</td>
<td className="p-3 text-center">{v.rating ?? '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Step 5: Environment Variables for Production
For Vercel deployment:
vercel env add PRICIUM_API_KEY
The next: { revalidate: 300 } option in the fetch call automatically enables ISR - pages are cached for 5 minutes and then regenerated on the next request.
What You've Built
A Next.js app that:
- Fetches variation-level product data server-side (no client API key exposure)
- Caches results with ISR to minimize API calls
- Renders a clean, sortable variations table
- Is ready to deploy on Vercel in minutes
