Putiikkipalvelu Docs
SDK

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:

ParamTypeRequiredDescription
takenumberYesNumber 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:

ParamTypeRequiredDescription
slugstringYesProduct 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:

ParamTypeRequiredDescription
slugsstring[]NoCategory 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:

ParamTypeRequiredDefaultDescription
slugsstring[]NoAll productsCategory slug hierarchy
pagenumberNo1Page number (1-based)
pageSizenumberNo12Products per page (max 100)
sortProductSortOptionNo"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:

OptionTypeDescription
signalAbortSignalCancel the request
cacheRequestCacheFetch cache mode
next.revalidatenumber | falseNext.js ISR revalidation time in seconds
next.tagsstring[]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 weight field 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 euros

Handling 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;
}

On this page