Интеграция Vendure GraphQL API с фронтендом

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Интеграция Vendure GraphQL API с фронтендом
Средняя
~5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Интеграция 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.