Інтеграція Shopify Storefront API з користувацьким фронтендом
Shopify Storefront API дозволяє використовувати Shopify як headless commerce backend — управління каталогом, корзиною та чекаутом через GraphQL, при цьому фронтенд повністю користувацький: Next.js, Nuxt, Astro, мобільний додаток чи будь-який інший клієнт.
Коли потрібен headless
Стандартна тема Shopify має обмеження:
- Структура URL жорстко визначена платформою
- Користувацький чекаут недоступний без Shopify Plus
- Складна анімація та нестандартні інтерфейси вимагають обходів обмежень Liquid
- Потрібна єдина вітрина для кількох Shopify-магазинів
- PWA або нативний мобільний додаток
Headless вирішує ці проблеми, але додає операційну складність: окремий хостинг, CI/CD, окремий деплой фронта.
Storefront API: аутентифікація
Для доступу до Storefront API потрібен Storefront API access token — публічний токен з обмеженими правами (тільки читання каталогу та мутації корзини):
Admin > Apps > Develop apps > [App] > Configuration > Storefront API access scopes
Токен передається в заголовку X-Shopify-Storefront-Access-Token — він може бути публічним (вбудований в JS-код фронтенду).
Базовий клієнт
// lib/shopify/client.ts
const SHOPIFY_DOMAIN = process.env.SHOPIFY_STORE_DOMAIN!;
const STOREFRONT_TOKEN = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
export async function storefrontFetch<T>({
query,
variables,
cache = 'force-cache',
tags,
}: {
query: string;
variables?: Record<string, unknown>;
cache?: RequestCache;
tags?: string[];
}): Promise<T> {
const res = await fetch(
`https://${SHOPIFY_DOMAIN}/api/2025-01/graphql.json`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': STOREFRONT_TOKEN,
},
body: JSON.stringify({ query, variables }),
cache,
next: tags ? { tags } : undefined,
}
);
if (!res.ok) throw new Error(`Storefront API error: ${res.status}`);
const { data, errors } = await res.json();
if (errors?.length) throw new Error(errors[0].message);
return data;
}
Отримання каталогу
// lib/shopify/queries/products.ts
const GET_PRODUCTS = `
query getProducts($first: Int!, $after: String, $sortKey: ProductSortKeys, $reverse: Boolean, $query: String) {
products(first: $first, after: $after, sortKey: $sortKey, reverse: $reverse, query: $query) {
edges {
cursor
node {
id
handle
title
availableForSale
priceRange {
minVariantPrice { amount currencyCode }
maxVariantPrice { amount currencyCode }
}
featuredImage {
url
altText
width
height
}
variants(first: 1) {
edges {
node {
id
availableForSale
selectedOptions { name value }
}
}
}
metafield(namespace: "custom", key: "badge") {
value
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
export async function getProducts({
first = 24,
after,
sortKey = 'RELEVANCE',
reverse = false,
query,
}: ProductsQueryParams) {
const data = await storefrontFetch<{ products: ProductConnection }>({
query: GET_PRODUCTS,
variables: { first, after, sortKey, reverse, query },
tags: ['products'],
});
return data.products;
}
Управління корзиною
Storefront API використовує Cart API (не застаріла Checkout API):
// lib/shopify/queries/cart.ts
const CREATE_CART = `
mutation cartCreate($input: CartInput) {
cartCreate(input: $input) {
cart {
id
checkoutUrl
lines(first: 100) {
edges {
node {
id
quantity
merchandise {
... on ProductVariant {
id
title
price { amount currencyCode }
product { title featuredImage { url altText } }
}
}
}
}
}
cost {
subtotalAmount { amount currencyCode }
totalAmount { amount currencyCode }
totalTaxAmount { amount currencyCode }
}
}
userErrors { field message }
}
}
`;
const ADD_TO_CART = `
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
cartLinesAdd(cartId: $cartId, lines: $lines) {
cart { id lines(first: 100) { edges { node { id quantity } } } }
userErrors { field message }
}
}
`;
export async function addToCart(cartId: string, variantId: string, quantity: number) {
return storefrontFetch({
query: ADD_TO_CART,
variables: {
cartId,
lines: [{ merchandiseId: variantId, quantity }]
},
cache: 'no-store',
});
}
Корзина зберігається на стороні Shopify, ID картри зберігається в cookie або localStorage. При переходу до оплати використовується cart.checkoutUrl — редирект на Shopify-чекаут.
Next.js App Router інтеграція
// app/products/[handle]/page.tsx
import { getProduct } from '@/lib/shopify';
import { AddToCartButton } from '@/components/AddToCartButton';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
const products = await getProducts({ first: 250 });
return products.edges.map(({ node }) => ({ handle: node.handle }));
}
export async function generateMetadata({ params }: { params: { handle: string } }) {
const product = await getProduct(params.handle);
if (!product) return {};
return {
title: product.title,
description: product.description.slice(0, 160),
openGraph: {
images: [{ url: product.featuredImage?.url }],
},
};
}
export default async function ProductPage({ params }: { params: { handle: string } }) {
const product = await getProduct(params.handle);
if (!product) notFound();
return (
<main>
<h1>{product.title}</h1>
<AddToCartButton product={product} />
</main>
);
}
Інкрементальна статична регенерація (ISR)
Каталог статично рендерится при збірці, оновлюється по вебхуку або по TTL:
// app/api/revalidate/route.ts — вебхук від Shopify
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(req: NextRequest) {
const hmac = req.headers.get('x-shopify-hmac-sha256');
// Верифікація HMAC...
const body = await req.json();
const topic = req.headers.get('x-shopify-topic');
if (topic === 'products/update' || topic === 'products/create') {
revalidateTag('products');
revalidateTag(`product-${body.handle}`);
}
if (topic === 'collections/update') {
revalidateTag('collections');
}
return new Response('OK');
}
Пошук та фільтрація
Storefront API підтримує query параметр з синтаксисом Shopify search:
// Фільтрація по тегу + ціні
const products = await getProducts({
query: 'tag:sale price:<5000',
sortKey: 'PRICE',
reverse: false,
});
// Пошук по назві
const results = await getProducts({
query: `title:*${searchTerm}*`,
});
Для складної фільтрації (фасети за характеристиками) — використовується collection.products з фільтрами:
collection(handle: "all") {
products(first: 24, filters: [
{ price: { min: 1000, max: 5000 } },
{ productMetafield: { namespace: "specifications", key: "material", value: "leather" } },
{ available: true }
]) { ... }
}
Інтернаціоналізація
Storefront API підтримує @inContext директиву для локалізації:
query getProduct($handle: String!, $country: CountryCode!, $language: LanguageCode!)
@inContext(country: $country, language: $language) {
product(handle: $handle) {
title
priceRange {
minVariantPrice { amount currencyCode }
}
}
}
Терміни
MVP headless-магазину на Next.js з каталогом, корзиною та чекаутом: 3–4 тижні. Повнофункціональний проект з CMS, мультирынком, користувацькою аналітикою, A/B-тестингом та CI/CD: 2–3 місяці.







