Интеграция Vendure GraphQL API с фронтендом
Vendure предоставляет два отдельных GraphQL endpoint: Shop API (/shop-api) для покупателей и Admin API (/admin-api) для администраторов. Фронтенд использует только Shop API. Аутентификация — через cookie-based сессии или Bearer token, выбор делается на уровне конфига сервера.
Настройка клиента (urql)
urql легче Apollo и лучше подходит для Vendure — меньше boilerplate при работе с cookie-сессиями:
// lib/vendureClient.ts
import {
createClient,
fetchExchange,
dedupExchange,
cacheExchange,
} from "urql";
export const shopClient = createClient({
url: `${process.env.NEXT_PUBLIC_VENDURE_API_URL}/shop-api`,
exchanges: [dedupExchange, cacheExchange, fetchExchange],
fetchOptions: {
credentials: "include", // cookie-based auth
headers: {
"vendure-token": process.env.NEXT_PUBLIC_CHANNEL_TOKEN!,
},
},
});
Для Bearer token (SPA без SSR):
fetchOptions: () => {
const token = localStorage.getItem("authToken");
return {
headers: { authorization: token ? `Bearer ${token}` : "" },
};
},
Генерация типов
# codegen.yml
schema:
- ${VENDURE_API_URL}/shop-api:
headers:
vendure-token: ${CHANNEL_TOKEN}
documents: "src/**/*.graphql"
generates:
src/generated/shop-types.ts:
plugins:
- typescript
- typescript-operations
- typescript-urql
config:
withHooks: true
scalars:
DateTime: "string"
JSON: "Record<string, unknown>"
Money: "number"
VENDURE_API_URL=http://localhost:3000 CHANNEL_TOKEN=my-token \
npx graphql-codegen --config codegen.yml
Каталог: запросы к Shop API
# queries/products.graphql
query GetProductList(
$options: ProductListOptions
) {
products(options: $options) {
totalItems
items {
id
name
slug
featuredAsset { preview }
variants {
id
name
priceWithTax
currencyCode
stockLevel
}
}
}
}
query GetProduct($slug: String!) {
product(slug: $slug) {
id
name
slug
description
featuredAsset { preview source }
assets { preview source }
variants {
id
name
sku
priceWithTax
currencyCode
stockLevel
options {
code
name
group { code name }
}
}
facetValues {
code
name
facet { code name }
}
}
}
// components/ProductGrid.tsx
import { useGetProductListQuery } from "@/generated/shop-types";
export function ProductGrid({ categorySlug }: { categorySlug: string }) {
const [{ data, fetching }] = useGetProductListQuery({
variables: {
options: {
filter: { facetValueFilters: [{ and: categorySlug }] },
sort: { name: SortOrder.Asc },
take: 24,
skip: 0,
},
},
});
if (fetching) return <ProductGridSkeleton />;
return (
<div className="grid grid-cols-4 gap-4">
{data?.products.items.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
Корзина: работа с активным заказом
В Vendure корзина — это Order в статусе AddingItems. Активный заказ привязан к сессии:
# mutations/order.graphql
mutation AddItemToOrder($variantId: ID!, $quantity: Int!) {
addItemToOrder(productVariantId: $variantId, quantity: $quantity) {
...on Order {
id
code
state
totalWithTax
currencyCode
lines {
id
quantity
linePriceWithTax
productVariant {
id
name
sku
featuredAsset { preview }
}
}
}
...on OrderModificationError { errorCode message }
...on OrderLimitError { errorCode message maxItems }
...on NegativeQuantityError { errorCode message }
...on InsufficientStockError { errorCode message quantityAvailable }
}
}
query GetActiveOrder {
activeOrder {
id
code
state
totalWithTax
subTotalWithTax
shippingWithTax
lines {
id
quantity
linePriceWithTax
productVariant { id name }
}
shippingLines {
shippingMethod { name description }
priceWithTax
}
discounts {
description
amountWithTax
}
}
}
Checkout flow
// hooks/useCheckout.ts
import { useMutation } from "urql";
import {
SetShippingAddressDocument,
SetShippingMethodDocument,
AddPaymentToOrderDocument,
TransitionOrderToStateDocument,
} from "@/generated/shop-types";
export function useCheckout() {
const [, setAddress] = useMutation(SetShippingAddressDocument);
const [, setShipping] = useMutation(SetShippingMethodDocument);
const [, addPayment] = useMutation(AddPaymentToOrderDocument);
const [, transition] = useMutation(TransitionOrderToStateDocument);
async function completeCheckout(params: CheckoutParams) {
// 1. Адрес
const addr = await setAddress({ input: params.address });
if (addr.data?.setOrderShippingAddress.__typename !== "Order") {
throw new Error(addr.data?.setOrderShippingAddress.message);
}
// 2. Метод доставки
await setShipping({ id: [params.shippingMethodId] });
// 3. Переход в ArrangingPayment
await transition({ state: "ArrangingPayment" });
// 4. Оплата
const payment = await addPayment({
input: {
method: "yookassa",
metadata: { returnUrl: `${window.location.origin}/checkout/confirm` },
},
});
if (payment.data?.addPaymentToOrder.__typename === "Order") {
return payment.data.addPaymentToOrder;
}
throw new Error(payment.data?.addPaymentToOrder.message);
}
return { completeCheckout };
}
Аутентификация
// hooks/useAuth.ts
const LOGIN = gql`
mutation Login($email: String!, $password: String!, $rememberMe: Boolean) {
login(username: $email, password: $password, rememberMe: $rememberMe) {
...on CurrentUser {
id
identifier
}
...on InvalidCredentialsError {
errorCode
message
}
...on NotVerifiedError {
errorCode
message
}
}
}
`;
const [, login] = useMutation(LOGIN);
const result = await login({ email, password, rememberMe: true });
if (result.data?.login.__typename === "CurrentUser") {
// сессия установлена через cookie, редирект
router.push("/account");
} else {
setError(result.data?.login.message);
}
Обработка ошибок Vendure
Vendure использует union types для ошибок — каждая мутация возвращает Result | ErrorType1 | ErrorType2. Паттерн обработки:
function assertIsOrder(result: AddItemToOrderResult): asserts result is Order {
if (result.__typename !== "Order") {
throw new VendureError(result.errorCode, result.message);
}
}
const result = await addToOrder({ variantId, quantity: 1 });
assertIsOrder(result.data!.addItemToOrder);
// Дальше TypeScript знает, что это Order
SSR с Next.js App Router
// app/shop/page.tsx
import { createServerClient } from "@/lib/vendureServerClient";
export default async function ShopPage() {
const client = createServerClient(); // клиент с credentials для SSR
const result = await client.query(GetProductListDocument, {
options: { take: 24 },
}).toPromise();
return <ProductGrid initialData={result.data} />;
}
Для SSR нужен отдельный серверный клиент без credentials: "include" — вместо него передаётся токен сессии из куки через cookie header.







