Интеграция 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 syntax:
// Фильтрация по тегу + цене
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 недели. Полноценный проект с поиском, фильтрацией, ISR, мультиязычностью, аналитикой и интеграцией CMS для контента: 2–3 месяца.







