Разработка интернет-магазина на Vendure
Vendure — headless commerce фреймворк на NestJS и TypeScript с GraphQL API. В отличие от commercetools, это open-source решение с self-hosted деплоем: вы контролируете инфраструктуру, данные и можете модифицировать ядро через плагины без форков. Архитектура основана на модулях NestJS, что делает расширение предсказуемым.
Когда Vendure — правильный выбор
- Нужен self-hosted: данные не покидают вашу инфраструктуру
- Требуется глубокая кастомизация бизнес-логики (налоги, доставка, промокоды)
- TypeScript — основной язык команды
- Бюджет не позволяет SaaS-платформы ($500+/мес на commercetools)
- Нужен контроль над схемой БД (PostgreSQL/MySQL)
Архитектура проекта
vendure-project/
├── src/
│ ├── vendure-config.ts # Главный конфиг
│ ├── plugins/ # Кастомные плагины
│ │ ├── loyalty/
│ │ ├── b2b-pricing/
│ │ └── erp-sync/
│ ├── email-handlers/ # Шаблоны писем
│ └── payment-handlers/ # Обработчики оплаты
├── storefront/ # Next.js / Nuxt
└── docker-compose.yml
Конфигурация Vendure
// src/vendure-config.ts
import { VendureConfig } from "@vendure/core";
import { defaultEmailHandlers, EmailPlugin } from "@vendure/email-plugin";
import { AssetServerPlugin } from "@vendure/asset-server-plugin";
import { AdminUiPlugin } from "@vendure/admin-ui-plugin";
export const config: VendureConfig = {
apiOptions: {
port: 3000,
adminApiPath: "admin-api",
shopApiPath: "shop-api",
adminApiPlayground: process.env.NODE_ENV === "development",
},
authOptions: {
tokenMethod: ["bearer", "cookie"],
superadminCredentials: {
identifier: process.env.SUPERADMIN_USERNAME!,
password: process.env.SUPERADMIN_PASSWORD!,
},
cookieOptions: {
secret: process.env.COOKIE_SECRET!,
},
},
dbConnectionOptions: {
type: "postgres",
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
synchronize: false, // только миграции в production
migrations: ["dist/migrations/*.js"],
},
paymentOptions: {
paymentMethodHandlers: [stripePaymentHandler, yookassaPaymentHandler],
},
taxOptions: {
taxCalculationStrategy: new CustomTaxCalculationStrategy(),
},
shippingOptions: {
shippingCalculators: [defaultShippingCalculator, tieredShippingCalculator],
shippingEligibilityCheckers: [defaultShippingEligibilityChecker],
fulfillmentHandlers: [manualFulfillmentHandler],
},
plugins: [
AssetServerPlugin.init({
route: "assets",
assetUploadDir: path.join(__dirname, "../static/assets"),
}),
EmailPlugin.init({
devMode: process.env.NODE_ENV === "development",
handlers: defaultEmailHandlers,
templatePath: path.join(__dirname, "../email/templates"),
transport: {
type: "smtp",
host: process.env.SMTP_HOST!,
port: 587,
auth: {
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!,
},
},
}),
AdminUiPlugin.init({
route: "admin",
port: 3002,
}),
LoyaltyPlugin,
B2bPricingPlugin,
ErpSyncPlugin,
],
};
Модель каналов и мультитенантность
Vendure поддерживает Channels — аналог мультиарендности. Один инстанс обслуживает несколько магазинов с раздельным каталогом, ценами и заказами:
// Канал создаётся через Admin API или скрипт заполнения
await channelService.create(ctx, {
code: "ru-channel",
token: "ru-token-abc123",
defaultCurrencyCode: CurrencyCode.RUB,
defaultLanguageCode: LanguageCode.ru,
defaultTaxZone: taxZoneRU,
defaultShippingZone: shippingZoneRU,
pricesIncludeTax: false,
});
Каждый запрос к Shop API должен содержать заголовок vendure-token: <channel-token>.
Checkout flow через Shop API
# 1. Добавить товар в заказ
mutation AddToOrder($productVariantId: ID!, $quantity: Int!) {
addItemToOrder(productVariantId: $productVariantId, quantity: $quantity) {
... on Order {
id
code
totalWithTax
lines {
id
quantity
productVariant { name sku }
unitPriceWithTax
}
}
... on ErrorResult {
errorCode
message
}
}
}
# 2. Установить адрес доставки
mutation SetShippingAddress($input: CreateAddressInput!) {
setOrderShippingAddress(input: $input) {
... on Order { id shippingAddress { fullName streetLine1 city } }
... on NoActiveOrderError { errorCode message }
}
}
# 3. Получить методы доставки и выбрать
query GetShippingMethods {
eligibleShippingMethods {
id
name
price
priceWithTax
description
}
}
mutation SetShippingMethod($id: [ID!]!) {
setOrderShippingMethod(shippingMethodId: $id) {
... on Order { id shipping shippingWithTax }
}
}
Платёжная интеграция (YooKassa)
// src/payment-handlers/yookassa.handler.ts
import { CreatePaymentResult, PaymentMethodHandler, LanguageCode } from "@vendure/core";
export const yookassaPaymentHandler = new PaymentMethodHandler({
code: "yookassa",
description: [{ languageCode: LanguageCode.ru, value: "YooKassa" }],
args: {
shopId: { type: "string" },
secretKey: { type: "string", ui: { component: "password-form-input" } },
},
async createPayment(ctx, order, amount, args, metadata): Promise<CreatePaymentResult> {
const yookassa = new YooKassa({
shopId: args.shopId,
secretKey: args.secretKey,
});
const payment = await yookassa.createPayment({
amount: {
value: (amount / 100).toFixed(2),
currency: order.currencyCode,
},
capture: true,
confirmation: {
type: "redirect",
return_url: `${process.env.SHOP_URL}/checkout/confirmation`,
},
description: `Заказ #${order.code}`,
metadata: { vendure_order_id: order.id },
});
return {
amount,
state: "Authorized",
transactionId: payment.id,
metadata: { confirmationUrl: payment.confirmation.confirmation_url },
};
},
async settlePayment(ctx, order, payment, args) {
// YooKassa с capture=true — оплата снимается автоматически
return { success: true };
},
async refundPayment(ctx, order, payment, args, lines, adjustment) {
const yookassa = new YooKassa({ shopId: args.shopId, secretKey: args.secretKey });
const refund = await yookassa.createRefund(payment.transactionId, {
amount: { value: (adjustment / 100).toFixed(2), currency: order.currencyCode },
});
return { state: "Settled", transactionId: refund.id };
},
});
Производительность и масштабирование
Vendure поддерживает Worker/Server разделение: тяжёлые задачи (email, экспорт, indexing) обрабатываются в отдельном Worker-процессе через Bull:
// server.ts
import { bootstrap } from "@vendure/core";
bootstrap(config);
// worker.ts — запускается отдельно
import { bootstrapWorker } from "@vendure/core";
bootstrapWorker(config);
Для Production: 2+ инстанса сервера (load balanced), 1+ воркера, Redis для очередей.
Этапы разработки и сроки
| Этап | Срок |
|---|---|
| Установка, конфигурация, БД, миграции | 2–3 дня |
| Импорт каталога (Products, Variants, Assets) | 3–7 дней |
| Кастомные плагины (налоги, доставка, промокоды) | 5–10 дней |
| Storefront (Next.js + GraphQL) | 10–20 дней |
| Платёжные интеграции (2–3 провайдера) | 4–6 дней |
| Admin UI кастомизация | 2–4 дня |
| Итого | 26–50 дней |







