Products
Fetch products using the Storefront SDK
Overview
The products resource provides methods for fetching product data, including latest products, product details, paginated listings, and product counts.
Methods
products.latest(take, options?)
Fetch the most recently added products that are in stock.
const products = await storefront.products.latest(6);Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
take | number | Yes | Number of products to return (1-100) |
Returns: Promise<Product[]>
products.getBySlug(slug, options?)
Fetch full product details for the product detail page.
const product = await storefront.products.getBySlug('gold-ring');Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Product URL slug |
Returns: Promise<ProductDetail>
Throws: NotFoundError if product not found
products.count(slugs?, options?)
Get the total number of products, optionally filtered by category.
// All products
const { count } = await storefront.products.count();
// Products in a category
const { count } = await storefront.products.count(['jewelry', 'rings']);Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
slugs | string[] | No | Category slug hierarchy |
Returns: Promise<ProductCountResponse>
products.sorted(params, options?)
Fetch paginated products with sorting. Includes total count for pagination.
const data = await storefront.products.sorted({
slugs: ['jewelry'],
page: 1,
pageSize: 12,
sort: 'newest'
});Parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
slugs | string[] | No | All products | Category slug hierarchy |
page | number | No | 1 | Page number (1-based) |
pageSize | number | No | 12 | Products per page (max 100) |
sort | ProductSortOption | No | "newest" | Sort order |
Sort Options:
"newest"- Most recently added first"price_asc"- Lowest price first"price_desc"- Highest price first"popularity"- Best sellers first
Returns: Promise<ProductListResponse>
products.filtered(params, options?)
Fetch filtered products (without total count). Use for sitemaps or simple listings.
const { products } = await storefront.products.filtered({
slugs: ['all-products'],
page: 1,
pageSize: 1000
});Parameters: Same as products.sorted()
Returns: Promise<{ name: string; products: Product[] }>
Fetch Options
All methods accept an optional options object:
| Option | Type | Description |
|---|---|---|
signal | AbortSignal | Cancel the request |
cache | RequestCache | Fetch cache mode |
next.revalidate | number | false | Next.js ISR revalidation time in seconds |
next.tags | string[] | Next.js cache tags for on-demand revalidation |
Response Types
Product
Used for product listings (cards, grids).
interface Product {
id: string;
name: string;
slug: string;
description: string;
price: number; // In cents
images: string[];
quantity: number | null; // null = unlimited
salePrice: number | null;
salePercent: string | null;
saleStartDate: string | null;
saleEndDate: string | null;
variations: ProductVariationListing[];
}ProductDetail
Full product for detail pages. Includes weight for shipping calculations:
interface ProductDetail extends Omit<Product, 'variations'> {
weight: number; // Weight in kg (default: 0.5)
sku: string | null;
metaTitle: string | null;
metaDescription: string | null;
categories: CategoryReference[];
variations: ProductVariation[]; // Full variations with options
}ProductVariation
Full variation with options (for product detail page):
interface ProductVariation {
id: string;
price: number;
salePrice: number | null;
salePercent: string | null;
saleStartDate: string | null;
saleEndDate: string | null;
quantity: number | null;
weight: number | null; // Weight in kg (null = inherit from product)
sku: string | null;
images: string[];
description: string | null;
showOnStore: boolean;
options: VariationOption[];
}
interface VariationOption {
value: string; // e.g., "Large"
optionType: {
name: string; // e.g., "Size"
};
}Weight Handling:
- Weight is only included in
ProductDetail(single product endpoint), not in listing endpoints - Products have a default weight of 0.5 kg if not specified
- Variations can have their own
weightfield to override the product weight - If a variation has
weight: null, it inherits the weight from its parent product - Use weight values for calculating shipping costs and filtering shipping methods by cart weight
ProductListResponse
Response from sorted():
interface ProductListResponse {
name: string; // Category name or "Kaikki tuotteeni"
products: Product[];
totalCount: number; // For pagination
}Examples
Homepage - Latest Products
import { storefront } from '@/lib/storefront';
export default async function Home() {
const products = await storefront.products.latest(6, {
next: { revalidate: 3600 } // Cache for 1 hour
});
return (
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}Product Listing Page with Pagination
import { storefront } from '@/lib/storefront';
export default async function ProductsPage({
searchParams
}: {
searchParams: { page?: string; sort?: string }
}) {
const page = Number(searchParams.page) || 1;
const sort = searchParams.sort || 'newest';
const { products, totalCount, name } = await storefront.products.sorted({
page,
pageSize: 12,
sort: sort as ProductSortOption
}, { cache: 'no-store' });
const totalPages = Math.ceil(totalCount / 12);
return (
<>
<h1>{name}</h1>
<ProductGrid products={products} />
<Pagination currentPage={page} totalPages={totalPages} />
</>
);
}Product Detail Page
import { storefront, NotFoundError } from '@/lib/storefront';
import { notFound } from 'next/navigation';
export default async function ProductPage({
params
}: {
params: { slug: string }
}) {
try {
const product = await storefront.products.getBySlug(params.slug, {
cache: 'no-store'
});
return <ProductDetail product={product} />;
} catch (error) {
if (error instanceof NotFoundError) {
notFound();
}
throw error;
}
}SEO Metadata for Product
import { Metadata } from 'next';
import { storefront, NotFoundError } from '@/lib/storefront';
export async function generateMetadata({
params
}: {
params: { slug: string }
}): Promise<Metadata> {
try {
const product = await storefront.products.getBySlug(params.slug);
return {
title: product.metaTitle || product.name,
description: product.metaDescription || product.description,
openGraph: {
images: product.images[0] ? [product.images[0]] : [],
},
};
} catch {
return { title: 'Product Not Found' };
}
}Sitemap Generation
import { storefront } from '@/lib/storefront';
export default async function sitemap() {
const { products } = await storefront.products.filtered({
slugs: ['all-products'],
page: 1,
pageSize: 1000
});
return products.map(product => ({
url: `https://mystore.com/product/${product.slug}`,
lastModified: new Date(),
}));
}Working with Sale Prices
function getDisplayPrice(product: Product): number {
const now = new Date();
// Check if sale is active
const isSaleActive =
product.salePrice !== null &&
(product.saleStartDate === null || new Date(product.saleStartDate) <= now) &&
(product.saleEndDate === null || new Date(product.saleEndDate) >= now);
return isSaleActive ? product.salePrice! : product.price;
}
// Usage
const displayPrice = getDisplayPrice(product) / 100; // Convert cents to eurosHandling Variations
'use client';
import { useState } from 'react';
import type { ProductDetail, ProductVariation } from '@putiikkipalvelu/storefront-sdk';
function ProductDetailClient({ product }: { product: ProductDetail }) {
const [selectedVariation, setSelectedVariation] = useState<ProductVariation | undefined>(
product.variations[0]
);
const currentPrice = selectedVariation?.price ?? product.price;
const currentImages = selectedVariation?.images.length
? selectedVariation.images
: product.images;
return (
<div>
{product.variations.length > 0 && (
<select
value={selectedVariation?.id}
onChange={(e) => {
const variation = product.variations.find(v => v.id === e.target.value);
setSelectedVariation(variation);
}}
>
{product.variations.map(v => (
<option key={v.id} value={v.id}>
{v.options.map(o => o.value).join(' / ')}
</option>
))}
</select>
)}
<p>Price: {(currentPrice / 100).toFixed(2)} €</p>
</div>
);
}Error Handling
import {
NotFoundError,
AuthError,
StorefrontError
} from '@putiikkipalvelu/storefront-sdk';
try {
const product = await storefront.products.getBySlug('my-product');
} catch (error) {
if (error instanceof NotFoundError) {
// Product not found (404)
return notFound();
}
if (error instanceof AuthError) {
// Invalid API key (401)
console.error('Check your STOREFRONT_API_KEY');
}
if (error instanceof StorefrontError) {
// Other API error
console.error(`API Error: ${error.message} (${error.code})`);
}
throw error;
}